├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── .nvmrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── cypress.config.ts
├── cypress
├── fixtures
│ └── example.json
├── plugins
│ └── index.js
└── support
│ ├── commands.js
│ ├── commands.ts
│ ├── component-index.html
│ ├── component.ts
│ └── index.js
├── doc
└── zh.md
├── docs
├── .vitepress
│ ├── components
│ │ ├── DragHandle.vue
│ │ ├── FruitExample.vue
│ │ ├── GroupExample.vue
│ │ ├── KanbanExample.vue
│ │ ├── LongListExample.vue
│ │ ├── PageListExample.vue
│ │ ├── ShorthandExample.vue
│ │ ├── SimpleGroupExample.vue
│ │ ├── SortableItem.vue
│ │ ├── SortableList.vue
│ │ └── utils.js
│ ├── config.ts
│ ├── style.styl
│ └── theme
│ │ └── index.js
├── basics.md
├── components
│ ├── draghandle.md
│ ├── slickitem.md
│ └── slicklist.md
├── drag-and-drop.md
├── getting-started.md
├── index.md
├── introduction.md
├── kanban.md
├── migrating-1x.md
├── public
│ ├── android-chrome-192x192.png
│ ├── android-chrome-256x256.png
│ ├── apple-touch-icon.png
│ ├── confetti.min.js
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── logo.png
│ ├── logo.svg
│ └── logomark.png
├── troubleshooting.md
└── window-scroll.md
├── example
├── Example.vue
├── components
│ ├── GroupExample.vue
│ ├── InnerList.vue
│ ├── SortableItem.vue
│ └── SortableList.vue
├── index.html
├── index.js
├── util
│ └── index.js
└── vite.config.js
├── index.d.ts
├── logo
├── android-chrome-192x192.png
├── android-chrome-256x256.png
├── apple-touch-icon.png
├── brand.png
├── demo.gif
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── logo.png
└── logomark.png
├── package.json
├── rollup.config.js
├── src
├── ContainerMixin.ts
├── ElementMixin.ts
├── HandleDirective.ts
├── Manager.ts
├── Sample.cy.jsx
├── SlicksortHub.ts
├── components
│ ├── DragHandle.ts
│ ├── SlickItem.ts
│ └── SlickList.ts
├── index.ts
├── plugin.ts
└── utils.ts
├── tsconfig.json
├── vite.config.ts
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "commonjs": {
4 | "presets": ["@babel/preset-env"]
5 | },
6 | "es6": {
7 | "presets": ["@babel/preset-env"],
8 | "plugins": ["transform-runtime"]
9 | },
10 | "rollup": {
11 | "presets": [["@babel/preset-env", { "modules": false }]]
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:@typescript-eslint/eslint-recommended",
5 | "plugin:@typescript-eslint/recommended",
6 | "plugin:vue/recommended",
7 | "plugin:cypress/recommended"
8 | ],
9 | "env": {
10 | "browser": true,
11 | "node": true,
12 | "mocha": true
13 | },
14 | "parser": "@typescript-eslint/parser",
15 | "plugins": [
16 | "@typescript-eslint"
17 | ],
18 | "parserOptions": {
19 | "sourceType": "module",
20 | "ecmaVersion": 2017,
21 | "ecmaFeatures": {
22 | "experimentalObjectRestSpread": true
23 | }
24 | },
25 | "rules": {
26 | "prefer-const": ["error"],
27 | "comma-dangle": ["error", "always-multiline"],
28 | "indent": ["error", 2, { "SwitchCase": 1 }],
29 | "semi": ["error", "always"],
30 | "no-mixed-operators": [
31 | "warn",
32 | {
33 | "groups": [
34 | ["&", "|", "^", "~", "<<", ">>", ">>>"],
35 | ["==", "!=", "===", "!==", ">", ">=", "<", "<="],
36 | ["in", "instanceof"]
37 | ],
38 | "allowSamePrecedence": true
39 | }
40 | ],
41 | "quotes": ["error", "single"],
42 | "eol-last": 2
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.DS_Store
2 | node_modules
3 | dist
4 | styles.min.css
5 | styles.min.css.map
6 | coverage
7 | npm-debug.log
8 | .out
9 | yarn-error.log
10 | .cache
11 | .temp
12 |
13 | # Cypress
14 | cypress/videos
15 | cypress/results
16 | cypress/screenshots
17 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github
2 | .babelrc
3 | coverage
4 | src
5 | test
6 | .*
7 | *.md
8 | server.js
9 | index.html
10 | /index.js
11 | karma.conf.js
12 | webpack.config.js
13 | rollup.config.js
14 | babel.preprocess.sass.js
15 | codecov.yml
16 | .travis.yml
17 | .out
18 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 5
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [2.0.5](https://github.com/Jexordexan/vue-slicksort/compare/v2.0.4...v2.0.5) (2023-02-12)
2 |
3 |
4 |
5 | ## [2.0.4](https://github.com/Jexordexan/vue-slicksort/compare/v2.0.3...v2.0.4) (2023-02-12)
6 |
7 |
8 |
9 | ## [2.0.3](https://github.com/Jexordexan/vue-slicksort/compare/v2.0.2...v2.0.3) (2022-11-25)
10 |
11 |
12 |
13 | ## [2.0.2](https://github.com/Jexordexan/vue-slicksort/compare/v2.0.1...v2.0.2) (2022-11-19)
14 |
15 |
16 |
17 | ## [2.0.1](https://github.com/Jexordexan/vue-slicksort/compare/v2.0.0...v2.0.1) (2022-11-19)
18 |
19 |
20 |
21 | # [2.0.0](https://github.com/Jexordexan/vue-slicksort/compare/v2.0.0-alpha.5...v2.0.0) (2022-11-19)
22 |
23 |
24 | ### Features
25 |
26 | * track game wins and moves ([ef7d36e](https://github.com/Jexordexan/vue-slicksort/commit/ef7d36e8235df9005f10c8699a4f791bc1ad96d9))
27 |
28 |
29 |
30 | # [2.0.0-alpha.5](https://github.com/Jexordexan/vue-slicksort/compare/v2.0.0-alpha.4...v2.0.0-alpha.5) (2021-05-04)
31 |
32 |
33 | ### Bug Fixes
34 |
35 | * Missing hub warning ([7396699](https://github.com/Jexordexan/vue-slicksort/commit/73966998f6b2bfb01f432672efde7d23f155be03))
36 |
37 |
38 | ### Features
39 |
40 | * Cancel drag on escape keypress ([c92833e](https://github.com/Jexordexan/vue-slicksort/commit/c92833e40bac858f2fc7b6330e854541751b21d7))
41 |
42 |
43 |
44 | # [2.0.0-alpha.4](https://github.com/Jexordexan/vue-slicksort/compare/v2.0.0-alpha.3...v2.0.0-alpha.4) (2021-02-24)
45 |
46 |
47 | ### Bug Fixes
48 |
49 | * animate nodes on drag out ([0a3c2be](https://github.com/Jexordexan/vue-slicksort/commit/0a3c2be16a5541ef8cf47eb4430faaba6ee52771))
50 | * remove helper class before transition to allow animation ([ab5aafd](https://github.com/Jexordexan/vue-slicksort/commit/ab5aafdbee69cf8a5135701f745b29633a8ff8f1))
51 | * useWindowAsScrollContainer ([0b65c19](https://github.com/Jexordexan/vue-slicksort/commit/0b65c1918b3c44c48d39f1e4f4d760fb7cdea65c)), closes [#88](https://github.com/Jexordexan/vue-slicksort/issues/88)
52 |
53 |
54 |
55 | # [2.0.0-alpha.3](https://github.com/Jexordexan/vue-slicksort/compare/v2.0.0-alpha.2...v2.0.0-alpha.3) (2021-02-19)
56 |
57 |
58 | ### Bug Fixes
59 |
60 | * Better guessing for "closest" destination ([b462c57](https://github.com/Jexordexan/vue-slicksort/commit/b462c579978b10d26ef3ba7d5ace6e94f95b093d))
61 | * drag settling animation ([cba65c3](https://github.com/Jexordexan/vue-slicksort/commit/cba65c38273034f64709fe157a681d6b77fd8f3e))
62 | * Prevent memory leaks by holding on to old helper refs ([2ef5d66](https://github.com/Jexordexan/vue-slicksort/commit/2ef5d66b05e87c4b14a9ac9663d298e701026157))
63 | * remove ghost reference on drag out ([d8e9adf](https://github.com/Jexordexan/vue-slicksort/commit/d8e9adfa285fa4e321c7bce9dd6e4a472b1f7925))
64 | * revert setup() to data() ([c0528e5](https://github.com/Jexordexan/vue-slicksort/commit/c0528e50016ea3940bc650e2d91aa937c75c747a))
65 | * timeout ssr compat ([06e9450](https://github.com/Jexordexan/vue-slicksort/commit/06e94507d61bf543f9b1955570a7ba2f85ed66ac))
66 | * XY sorting bug when n=1 ([819bd4b](https://github.com/Jexordexan/vue-slicksort/commit/819bd4bf758771cbfe03a252e64a3977df309d8e))
67 |
68 |
69 | ### Features
70 |
71 | * Add #item slot support for SlickList ([33ac30d](https://github.com/Jexordexan/vue-slicksort/commit/33ac30d264d71abb5d976874d127a5b53493452f))
72 | * Animate nodes when dragging out ([962a706](https://github.com/Jexordexan/vue-slicksort/commit/962a706da08994234b27c16ba986f976737b061d))
73 | * DragHandle component ([ab88102](https://github.com/Jexordexan/vue-slicksort/commit/ab881027c7dee6938a153961ec876d1217af220f))
74 |
75 |
76 |
77 | # [2.0.0-alpha.2](https://github.com/Jexordexan/vue-slicksort/compare/v1.2.0...v2.0.0-alpha.2) (2021-02-16)
78 |
79 |
80 | ### Bug Fixes
81 |
82 | * Account for margin in drag and drop ([0c571bb](https://github.com/Jexordexan/vue-slicksort/commit/0c571bbce89e559c956b8809e41bd99aa22a251e))
83 | * Account for scroll change when transitioning helper ([9a4c0c7](https://github.com/Jexordexan/vue-slicksort/commit/9a4c0c7eb9bf6b50ff55b720324a4dff967b6591))
84 | * change setup() to data() ([6a5617d](https://github.com/Jexordexan/vue-slicksort/commit/6a5617de2b60513bac46f5dd7efd5dc0d407d898))
85 | * components rendering with imported h ([daf9d09](https://github.com/Jexordexan/vue-slicksort/commit/daf9d0993517034e40cc1a3ea2d950bdc732c54e))
86 | * drag-n-drop for vue 3 ([9f603d3](https://github.com/Jexordexan/vue-slicksort/commit/9f603d346b9e725ccf64d91970b5fa083ce625ce))
87 | * Dragging between containers ([849aa8c](https://github.com/Jexordexan/vue-slicksort/commit/849aa8cd3cc48f4baf77e6cd94060f8ebf764986))
88 | * nodes is undefined ([7e2409d](https://github.com/Jexordexan/vue-slicksort/commit/7e2409d7f167a220dbfc6533009267e79bdd24db))
89 | * references to this.getOffset no longer valid ([94816c0](https://github.com/Jexordexan/vue-slicksort/commit/94816c0ca0df0a9d2547b02a369fc5f1a84b6a32))
90 | * scroll compensation and node offset ([69fa066](https://github.com/Jexordexan/vue-slicksort/commit/69fa0666ab5c7b82135fe7cda189913d5d62d526))
91 | * scrolling on mobile broken because of passive events ([37630b2](https://github.com/Jexordexan/vue-slicksort/commit/37630b2adec11dcaba40515ecf8519db8a68b8b9))
92 |
93 |
94 | ### Features
95 |
96 | * add drag handle to plugin ([2b088a1](https://github.com/Jexordexan/vue-slicksort/commit/2b088a11272851fd81cd636fc22ed9179c86ac65))
97 | * add support for allow and block props ([e032282](https://github.com/Jexordexan/vue-slicksort/commit/e0322824a721e0a7b37785142986e7545480d074))
98 | * Allow :accept="true" as a prop ([d794cd7](https://github.com/Jexordexan/vue-slicksort/commit/d794cd7cbdf54af20e0e8320a096d2dbdcb70136))
99 | * change modelValue to list prop ([d2c2488](https://github.com/Jexordexan/vue-slicksort/commit/d2c24886288b594e9447003baae08cda58569021))
100 | * working drag between groups ([aa11244](https://github.com/Jexordexan/vue-slicksort/commit/aa1124454ceaba818d6b15ebf3cbd3777ed85a1b))
101 |
102 |
103 |
104 | # [2.0.0-alpha.1](https://github.com/Jexordexan/vue-slicksort/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2021-01-22)
105 |
106 |
107 | ### Bug Fixes
108 |
109 | * Handle Directive hook ([86d07d3](https://github.com/Jexordexan/vue-slicksort/commit/86d07d3107325e6afa59badc15c5876d372daeaa))
110 |
111 |
112 |
113 | # [2.0.0-alpha.0](https://github.com/Jexordexan/vue-slicksort/compare/v1.1.3...v2.0.0-alpha.0) (2020-12-01)
114 |
115 |
116 | ### Features
117 |
118 | * Upgrade to Vue 3 ([e837d39](https://github.com/Jexordexan/vue-slicksort/commit/e837d3958e56b0571d48b7e2ddcd9c881e81e23b))
119 |
120 |
121 | ### BREAKING CHANGES
122 |
123 | * No longer works in Vue 2
124 |
125 |
126 |
127 | ## Changelog
128 |
129 | ## 1.2.0
130 |
131 | - feat: `tag` props on SlickList and SlickItem components
132 | - fix: Dragging broken on Android when distance property is set
133 | - fix: node helper not clean
134 | - fix: add event listeners in passive mode
135 | - fix: stopPropogation on click events
136 | - fix: .d.ts types
137 | - docs: Chinese documentation
138 | - chore: lots of dependencies
139 |
140 | ## 1.1.0
141 |
142 | - ADDED appendTo prop - appends the helper to the selected DOM node.
143 |
144 | ## 1.0.0
145 |
146 | - ADDED Typescript support
147 | - ADDED Settling animation, the drag helper will now settle back into its new location after releasing the drag.
148 | - BREAKING: sortStart, sortMove, and sortEnd events are now kebab-case (sort-start, sort-move, sort-end) as per standard practice.
149 | - Any previous event listeners will need to be updated
150 |
151 | ### 0.1.5
152 |
153 | - Fixed a bug in firefox where the text got selected upon dragging
154 |
155 | ### 0.1.1
156 |
157 | - Fixed bug where the node index wasn't being updated on the manager
158 |
159 | ### 0.1.0
160 |
161 | - Initial push: Convert library from react to vue.
162 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018, Jordan Simonds
4 |
5 | Copyright for portions of this project are held by Claudéric Demers, 2016, as part of project react-sortable-hoc.
6 | All other copyright for this project are held by Jordan Simonds, 2018.
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy
9 | of this software and associated documentation files (the "Software"), to deal
10 | in the Software without restriction, including without limitation the rights
11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the Software is
13 | furnished to do so, subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | SOFTWARE.
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue Slicksort 🖖
2 |
3 | 
4 |
5 | > A set of component mixins to turn any list into an animated, touch-friendly, sortable list.
6 | > Based on [react-sortable-hoc](https://github.com/clauderic/react-sortable-hoc) by [@clauderic]
7 |
8 | [](https://www.npmjs.com/package/vue-slicksort)
9 | [](https://www.npmjs.com/package/vue-slicksort)
10 | [](https://github.com/Jexordexan/vue-slicksort/blob/master/LICENSE)
11 | 
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ### Examples available here: [vue-slicksort.netlify.app/](https://vue-slicksort.netlify.app/)
20 |
21 | ### [中文文档](./doc/zh.md)
22 |
23 | ## Features
24 |
25 | - **`v-model` Compatible** – Make any array editable with the `v-model` standard
26 | - **Mixin Components** – Integrates with your existing components
27 | - **Standalone Components** – Easy to use components for slick lists
28 | - **Drag handle, auto-scrolling, locked axis, events, and more!**
29 | - **Suuuper smooth animations** – Chasing the 60FPS dream 🌈
30 | - **Horizontal lists, vertical lists, or a grid** ↔ ↕ ⤡
31 | - **Touch support** 👌
32 | - **Oh yeah, and it's DEPENDENCY FREE!** 👌
33 |
34 | ## Installation
35 |
36 | Using [npm](https://www.npmjs.com/package/vue-slicksort):
37 |
38 | ```
39 | $ npm install vue-slicksort --save
40 | ```
41 |
42 | Using yarn:
43 |
44 | ```
45 | $ yarn add vue-slicksort
46 | ```
47 |
48 | Using a CDN:
49 |
50 | ```html
51 |
52 | ```
53 |
54 | Then, using a module bundler that supports either CommonJS or ES2015 modules, such as [webpack](https://github.com/webpack/webpack):
55 |
56 | ```js
57 | // Using an ES6 transpiler like Babel
58 | import { ContainerMixin, ElementMixin } from 'vue-slicksort';
59 |
60 | // Not using an ES6 transpiler
61 | var slicksort = require('vue-slicksort');
62 | var ContainerMixin = slicksort.ContainerMixin;
63 | var ElementMixin = slicksort.ElementMixin;
64 | ```
65 |
66 | If you are loading the package via `
72 | ```
73 |
74 | ## Usage
75 |
76 | Check out the docs: [vue-slicksort.netlify.app](https://vue-slicksort.netlify.app/)
77 |
78 |
161 |
162 |
163 |
164 | ## Why should I use this?
165 |
166 | There are already a number of great Drag & Drop libraries out there (for instance, [vuedraggable](https://github.com/SortableJS/Vue.Draggable) is fantastic). If those libraries fit your needs, you should definitely give them a try first. However, most of those libraries rely on the HTML5 Drag & Drop API, which has some severe limitations. For instance, things rapidly become tricky if you need to support touch devices, if you need to lock dragging to an axis, or want to animate the nodes as they're being sorted. Vue Slicksort aims to provide a simple set of component mixins to fill those gaps. If you're looking for a dead-simple, mobile-friendly way to add sortable functionality to your lists, then you're in the right place.
167 |
404 |
405 | # FAQ
406 |
407 |
415 |
416 |
417 | ### Upgrade from v1.x
418 |
419 | There are a few changes in v2, mainly support for Vue 3 and dragging between groups. Read more about migrating here:
420 | [vue-slicksort.netlify.app/migrating-1x](https://vue-slicksort.netlify.app/migrating-1x)
421 |
422 | ### Upgrade from v0.x
423 |
424 | The event names have all changed from camelCase to dash-case to accommodate for inline HTML templates.
425 |
426 | ### Grid support?
427 |
428 | Need to sort items in a grid? We've got you covered! Just set the `axis` prop to `xy`. Grid support is currently limited to a setup where all the cells in the grid have the same width and height, though we're working hard to get variable width support in the near future.
429 |
430 | ### Item disappearing when sorting / CSS issues
431 |
432 | Upon sorting, `vue-slicksort` creates a clone of the element you are sorting (the _sortable-helper_) and appends it to the end of the `appendTo` tag. The original element will still be in-place to preserve its position in the DOM until the end of the drag (with inline-styling to make it invisible). If the _sortable-helper_ gets messed up from a CSS standpoint, consider that maybe your selectors to the draggable item are dependent on a parent element which isn't present anymore (again, since the _sortable-helper_ is at the end of the `appendTo` prop). This can also be a `z-index` issue, for example, when using `vue-slicksort` within a Bootstrap modal, you'll need to increase the `z-index` of the SortableHelper so it is displayed on top of the modal.
433 |
434 | ### Click events being swallowed
435 |
436 | By default, `vue-slicksort` is triggered immediately on `mousedown`. If you'd like to prevent this behaviour, there are a number of strategies readily available. You can use the `distance` prop to set a minimum distance (in pixels) to be dragged before sorting is enabled. You can also use the `pressDelay` prop to add a delay before sorting is enabled. Alternatively, you can also use the [HandleDirective](https://github.com/Jexordexan/vue-slicksort/blob/master/src/HandleDirective.js).
437 |
438 | ### Scoped styles
439 |
440 | If you are using scoped styles on the sortable list, you can use `appendTo` prop.
441 |
442 | ## Dependencies
443 |
444 | Slicksort has no dependencies.
445 | `vue` is the only peerDependency
446 |
447 | ## Reporting Issues
448 |
449 | If believe you've found an issue, please [report it](https://github.com/Jexordexan/vue-slicksort/issues) along with any relevant details to reproduce it. The easiest way to do so is to fork this [jsfiddle](https://jsfiddle.net/Jexordexan/1puv2L6c/).
450 |
451 | ## Asking for help
452 |
453 | Please file an issue for personal support requests. Tag them with `question`.
454 |
455 | ## Contributions
456 |
457 | Yes please! Feature requests / pull requests are welcome.
458 |
459 | ## Thanks
460 |
461 | This library is heavily based on [react-sortable-hoc](https://github.com/clauderic/react-sortable-hoc) by Claudéric Demers (@clauderic). A very simple and low overhead implementation of drag and drop that looks and performs great!
462 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 |
3 | export default defineConfig({
4 | component: {
5 | setupNodeEvents(on, config) {},
6 | specPattern: "src/**/*cy.*",
7 | },
8 |
9 | component: {
10 | devServer: {
11 | framework: "vue",
12 | bundler: "vite",
13 | },
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const { startDevServer } = require('@cypress/vite-dev-server');
3 | const vue = require('@vitejs/plugin-vue');
4 |
5 | module.exports = (on, config) => {
6 | const viteConfig = {
7 | plugins: [vue()],
8 | };
9 |
10 | viteConfig.esbuild = viteConfig.esbuild || {};
11 | viteConfig.esbuild.jsxFactory = 'h';
12 | viteConfig.esbuild.jsxFragment = 'Fragment';
13 | viteConfig.logLevel = 'error';
14 | viteConfig.resolve = {
15 | alias: {
16 | vue: 'vue/dist/vue.esm-bundler.js',
17 | },
18 | };
19 |
20 | on('dev-server:start', (options) => {
21 | return startDevServer({ options, viteConfig });
22 | });
23 | return config;
24 | };
25 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | Cypress.Commands.add('drag', { prevSubject: 'element' }, (subject, options) => {
18 | if (options.x && options.y) {
19 | cy.get(subject).trigger('mousedown', { which: 1 });
20 | cy.document()
21 | .trigger('mousemove', {
22 | pageX: options.x,
23 | pageY: options.y,
24 | })
25 | .trigger('mouseup', { force: true });
26 | }
27 | });
28 | //
29 | //
30 | // -- This is a dual command --
31 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
32 | //
33 | //
34 | // -- This will overwrite an existing command --
35 | Cypress.Commands.overwrite('get', (originalFn, selector, options) => {
36 | if (typeof selector === 'string') {
37 | return originalFn(selector.replace(/cy:(\w+)/g, '[data-cy="$1"]'), options);
38 | } else {
39 | return originalFn(selector, options);
40 | }
41 | });
42 |
--------------------------------------------------------------------------------
/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************
3 | // This example commands.ts shows you how to
4 | // create various custom commands and overwrite
5 | // existing commands.
6 | //
7 | // For more comprehensive examples of custom
8 | // commands please read more here:
9 | // https://on.cypress.io/custom-commands
10 | // ***********************************************
11 | //
12 | //
13 | // -- This is a parent command --
14 | // Cypress.Commands.add('login', (email, password) => { ... })
15 | //
16 | //
17 | // -- This is a child command --
18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
19 | //
20 | //
21 | // -- This is a dual command --
22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
23 | //
24 | //
25 | // -- This will overwrite an existing command --
26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
27 | //
28 | // declare global {
29 | // namespace Cypress {
30 | // interface Chainable {
31 | // login(email: string, password: string): Chainable
32 | // drag(subject: string, options?: Partial): Chainable
33 | // dismiss(subject: string, options?: Partial): Chainable
34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
35 | // }
36 | // }
37 | // }
--------------------------------------------------------------------------------
/cypress/support/component-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Components App
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/cypress/support/component.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/component.ts is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
22 | import { mount } from 'cypress/vue'
23 |
24 | // Augment the Cypress namespace to include type definitions for
25 | // your custom command.
26 | // Alternatively, can be defined in cypress/support/component.d.ts
27 | // with a at the top of your spec.
28 | declare global {
29 | namespace Cypress {
30 | interface Chainable {
31 | mount: typeof mount
32 | }
33 | }
34 | }
35 |
36 | Cypress.Commands.add('mount', mount)
37 |
38 | // Example use:
39 | // cy.mount(MyComponent)
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 | // Alternatively you can use CommonJS syntax:
19 | // require('./commands')
20 |
21 | beforeEach(() => {
22 | cy.viewport(600, 600, { log: false });
23 | });
24 |
--------------------------------------------------------------------------------
/doc/zh.md:
--------------------------------------------------------------------------------
1 | ## 功能
2 |
3 | - **`v-model` Compatible** – 依照 `v-model` 标准让所有数组可编辑
4 | - **Mixin Components** – 使用 `Mixin` 与你现有的组件集成
5 | - **Standalone Components** – 对于简单的列表直接使用现成组件
6 | - **Drag handle, auto-scrolling, locked axis, events, and more!** - 拖拽 handle,自动滚动,坐标轴锁定,事件等等
7 | - **Suuuper smooth animations** – 追逐 60FPS 的梦想 🌈
8 | - **Horizontal lists, vertical lists, or a grid** ↔ ↕ ⤡ - 支持水平列表,垂直列表和网格
9 | - **Touch support** 👌- 支持触控操作
10 | - **Oh yeah, and it's DEPENDENCY FREE!** 👌- 没有依赖其他库
11 |
12 | ## 安装
13 |
14 | 使用 [npm](https://www.npmjs.com/package/vue-slicksort):
15 |
16 | ```
17 | $ npm install vue-slicksort --save
18 | ```
19 |
20 | 使用 yarn:
21 |
22 | ```
23 | $ yarn add vue-slicksort
24 | ```
25 |
26 | 使用 CDN:
27 |
28 | ```html
29 |
30 | ```
31 |
32 | 然后,使用支持 CommonJS 或者 ES2015 modules 的模块打包工具,例如 [webpack](https://github.com/webpack/webpack):
33 |
34 | ```js
35 | // 使用类似 Babel 这样的 ES6 编译工具
36 | import { ContainerMixin, ElementMixin } from 'vue-slicksort';
37 |
38 | // 不使用 ES6 的编译工具
39 | var slicksort = require('vue-slicksort');
40 | var ContainerMixin = slicksort.ContainerMixin;
41 | var ElementMixin = slicksort.ElementMixin;
42 | ```
43 |
44 | 如果你通过 `
50 | ```
51 |
52 | ## 用法
53 |
54 | ### 基础示例
55 |
56 | ```js
57 | import Vue from 'vue';
58 | import { ContainerMixin, ElementMixin } from 'vue-slicksort';
59 |
60 | const SortableList = {
61 | mixins: [ContainerMixin],
62 | template: `
63 |
66 | `,
67 | };
68 |
69 | const SortableItem = {
70 | mixins: [ElementMixin],
71 | props: ['item'],
72 | template: `
73 | {{item}}
74 | `,
75 | };
76 |
77 | const ExampleVue = {
78 | name: 'Example',
79 | template: `
80 |
81 |
82 |
83 |
84 |
85 | `,
86 | components: {
87 | SortableItem,
88 | SortableList,
89 | },
90 | data() {
91 | return {
92 | items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6', 'Item 7', 'Item 8'],
93 | };
94 | },
95 | };
96 |
97 | const app = new Vue({
98 | el: '#root',
99 | render: (h) => h(ExampleVue),
100 | });
101 | ```
102 |
103 | 就这样!默认情况下,Vue Slickort 不附带任何样式,因为它的目的是在现有组件上做扩展。
104 |
105 | ## Slicksort components
106 |
107 | 为了实现两个 mixins 预先设置了两个组件,使用方式如下:
108 |
109 | ```javascript
110 | import { SlickList, SlickItem } from 'vue-slicksort';
111 |
112 | const ExampleVue = {
113 | name: 'Example',
114 | template: `
115 |
116 |
117 |
118 | {{ item }}
119 |
120 |
121 |
122 | `,
123 | components: {
124 | SlickItem,
125 | SlickList,
126 | },
127 | data() {
128 | return {
129 | items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6', 'Item 7', 'Item 8'],
130 | };
131 | },
132 | };
133 | ```
134 |
135 | ## 为什么我要用这个?
136 |
137 | 现在已经有一些很好的(Drag & Drop)拖放库了(例如, [vuedraggable](https://github.com/SortableJS/Vue.Draggable) 就很棒),如果这些库能满足你的需求,你应该先试一下他们。然而,大部分的库依赖 HTML5 的(Drag & Drop)拖放 API,会有一些严重的兼容或限制问题。例如,如果你需要支持触控设备,再如果你需要锁定只在一个坐标方向上拖动,或者想要在节点进行排序时有动画,这些都会变得很麻烦。Vue Slicksort 提供了一系列简单的组件混入旨在解决之前提到的这些问题。如果你正在寻找一种非常简单、移动友好的方式将可排序功能添加到你的列表中,那么你算是来对地方了。
138 |
139 | ## 定制化和属性集
140 |
141 | 你可以在任何使用了 `ContainerMixin` 的组件上应用单一的 `props` 选项,在执行排序期间,该组件还会发出几个事件。下面是一个自定义组件的示例
142 |
143 | You apply options as individual `props` on whatever component is using the `ContainerMixin`. The component also emits several events during a sorting operation. Here's an example of a customized component:
144 |
145 | ```html
146 |
153 |
154 |
155 | ```
156 |
157 | ## `ContainerMixin` 外层容器的 mixin
158 |
159 | ### Props
160 |
161 | #### `value` _(required)_
162 |
163 | type: _Array_
164 |
165 | `value` 可以从`v-model`继承,但必须设置为 `Container` 里面 `v-for` 遍历的列表相同的列表
166 |
167 | #### `axis`
168 |
169 | type: _String_
170 |
171 | default: `y`
172 |
173 | 条目能按照水平,垂直,或者网格进行拖动,可选值有: `x`, `y` or `xy`
174 |
175 | #### `lockAxis`
176 |
177 | type: _String_
178 |
179 | 当在排序时,你可以按照你的意愿锁定一个坐标轴方向进行移动,这是 HTML5 的 Drag & Drop 做不到的。可选值有: `x`, `y` or `xy`
180 |
181 | #### `helperClass`
182 |
183 | type: _String_
184 |
185 | 如果你想给排序中的元素添加一些样式(注意:这个 class 是加给正在排序的那个拖起来的元素的),可以用这个属性。
186 |
187 | #### `appendTo`
188 |
189 | type: _String_
190 |
191 | default: `body`
192 |
193 | 如果你想给正在排序的元素指定一个父元素并添加进去,可以使用这个属性,提供一个字符串的 querySelector 即可,默认是添加到 body 中的。
194 |
195 | #### `transitionDuration`
196 |
197 | type: _Number_
198 |
199 | default: `300`
200 |
201 | 被拖动的元素移动时,其他元素的过渡持续时间(即 transition 属性的 duration),如果你想把过渡动画取消,可以设置成 0 。
202 |
203 | #### `draggedSettlingDuration`
204 |
205 | type: _Number_
206 |
207 | default: `null`
208 |
209 | 覆写拖动的元素放开然后到安放位置的动画间隔时间(即放开拖拽后到元素进入安放位置的过渡时间),如果不设置,会按照 `transitionDuration` 的设置。
210 |
211 | #### `pressDelay`
212 |
213 | type: _Number_
214 |
215 | default: `0`
216 |
217 | 如果你希望元素仅在按下一定时间后才可排序,更改此属性即可。 移动端合理的值是 `200`,此属性不能与 `distance` 一起使用。(这个属性可以用来解决拖动元素上有点击事件,但是点击事件不生效的情况)
218 |
219 | #### `pressThreshold`
220 |
221 | type: _Number_
222 |
223 | default: `5`
224 |
225 | 忽略 press 事件之前可容忍的运动像素数
226 |
227 | #### `distance`
228 |
229 | type: _Number_
230 |
231 | default: `0`
232 |
233 | 如果你希望元素仅在拖动一定的像素(即拖动一定距离)后才可排序,可以使用这个属性,此属性不能与 `pressDelay` 一起使用。(这个属性可以用来解决拖动元素上有点击事件,但是点击事件不生效的情况,个人感觉比 pressDelay 更合适)
234 |
235 | #### `useDragHandle`
236 |
237 | type: _Boolean_
238 |
239 | default: `false`
240 |
241 | 如果你使用的是 `HandleDirective` ,将其设置为 `true`
242 |
243 | #### `useWindowAsScrollContainer`
244 |
245 | type: _Boolean_
246 |
247 | default: `false`
248 |
249 | 如果需要,可以将 `window` 设置为滚动容器。(只有 `Container` 出现滚动条时才能看出效果)
250 |
251 | #### `hideSortableGhost`
252 |
253 | type: _Boolean_
254 |
255 | default: `true`
256 |
257 | 控制是否自动隐藏 ghost 元素。默认情况下,为方便起见,Vue Slicksort 列表将自动隐藏当前正在排序的元素。 如果你想应用自己的样式,请将其设置为 false 。
258 |
259 | #### `lockToContainerEdges`
260 |
261 | type: _Boolean_
262 |
263 | default: `false`
264 |
265 | 你可以将可排序元素的移动锁定到其父容器 `Container`。
266 |
267 | #### `lockOffset`
268 |
269 | type: _`OffsetValue` or [ `OffsetValue`, `OffsetValue` ]_\*
270 |
271 | default: `"50%"`
272 |
273 | 当 `lockToContainerEdges` 设置为 `true`,这个属性可以控制可排序 helper(即拖起来的那个元素)与其父容器 `Container` 的顶部/底部边缘之间的偏移距离。百分比的值是相对于当前正在排序的项目的高度,如果希望为锁定容器的顶部和底部指定不同的行为,还可以传入一个 `array`(例如:`["0%","100%"]`)。
274 |
275 | \* `OffsetValue` 可以是有限 `Number` ,也可以是由数字和单位(`px` or `%`)组成的 `String` 。 例如: `10` (与 `"10px"`相同), `"50%"`
276 |
277 | #### `shouldCancelStart`
278 |
279 | type: _Function_
280 |
281 | default: [Function](https://github.com/Jexordexan/vue-slicksort/blob/main/src/ContainerMixin.js#L41)
282 |
283 | 该函数在排序开始之前被调用,可以用于编程方式在排序开始之前取消排序。 默认情况下,如果事件目标`input`, `textarea`, `select` 或 `option`,它将取消排序。
284 |
285 | #### `getHelperDimensions`
286 |
287 | type: _Function_
288 |
289 | default: [Function](https://github.com/Jexordexan/vue-slicksort/blob/main/src/ContainerMixin.js#L49)
290 |
291 | 可选的 `function({node, index, collection})` 返回 SortableHelper 的计算尺寸,有关更多详细信息,请参见[默认实现](https://github.com/Jexordexan/vue-slicksort/blob/main/src/ContainerMixin.js#L49)。
292 |
293 | ### Events
294 |
295 | 事件是从 Container 元素发出的,可以使用 `v-bind` 或 `@ `指令绑定。
296 |
297 | #### `@sort-start`
298 |
299 | emits: `{ event: MouseEvent, node: HTMLElement, index: number, collection: string }`
300 |
301 | 排序开始时触发。
302 |
303 | #### `@sort-move`
304 |
305 | emits: `{ event }`
306 |
307 | 在排序期间移动鼠标时触发。
308 |
309 | #### `@sort-end`
310 |
311 | emits: `{ event, newIndex, oldIndex, collection }`
312 |
313 | 排序结束时触发。
314 |
315 | #### `@input`
316 |
317 | emits: `Array`
318 |
319 | 在排序结束然后新排序的列表生成后触发。
320 |
321 | ---
322 |
323 | ## `ElementMixin`
324 |
325 | ### Props
326 |
327 | #### `index` _(required)_
328 |
329 | type: _Number_
330 |
331 | 这是元素在列表中的排序索引,是必填属性。
332 |
333 | #### `collection`
334 |
335 | type: _Number or String_
336 |
337 | default: `0`
338 |
339 | 元素所属的集合。如果你在同一 `Container` 中有多组可排序元素,这将很有用。[例子](http://Jexordexan.github.io/vue-slicksort/#/basic-configuration/multiple-lists)
340 |
341 | #### `disabled`
342 |
343 | type: _Boolean_
344 |
345 | default: `false`
346 |
347 | 元素是否可排序
348 |
349 | ## `HandleDirective`
350 |
351 | `v-handle` 指令在可拖动元素内部使用。(即用了这个指令,可以让拖动只在元素的某个位置生效)
352 |
353 | `Container` 必须由 `:useDragHandle` 属性,且设置为 `true` 时才能正常工作。
354 |
355 | 这里有关于此的一个简单元素的例子
356 |
357 | ```html
358 |
359 |
360 |
361 |
362 | {{item.value}}
363 |
364 |
365 |
366 |
374 | ```
375 |
376 | # FAQ
377 |
378 |
386 |
387 | ### Upgrade from v0.x.x
388 |
389 | 事件名已经全部从驼峰命名更改为中划线分隔(dash-case),以适应内联 HTML 模板。
390 |
391 | ### Grid support?
392 |
393 | 需要排序一个网格中的元素?我们已经实现了,将 `axis` 属性设置成 `xy`即可,当前仅限于支持所有格子设置相同宽度和高度的网格。我们会继续努力在不久的未来支持可变宽度的支持
394 |
395 | ### Item disappearing when sorting / CSS issues (拖动元素消失)
396 |
397 | 在进行排序时,`vue-slicksort` 创建一个你正在排序的元素的克隆元素(即上面一直提到的 _sortable-helper_ ),然后添加到 `appendTo` 属性设置的标签的末尾。原始的元素会一直在 DOM 中保持在原来的位置直到拖动结束(使用了内联样式使其不可见)。如果从 CSS 角度来看, _sortable-helper_ 出了问题,可能是因为你的 draggable 元素的选择器依赖了一个不存在的父元素(还是刚才说的,因为 _sortable-helper_ 位于 `appendTo` 属性设置的标签的末尾)。也可能是 `z-index` 的问题,例如,在一个 Bootstrap modal(弹框)中使用 `vue-slicksort` ,如果想让 _sortable-helper_ 在弹框中展示出来,你就得增加它的 `z-index` ,来提高层级。
398 |
399 | ### Click events being swallowed(Click 事件无效)
400 |
401 | 默认情况,`vue-slicksort` 在 `mousedown` 时立即出发。如果你想要阻止这种行为,有一些现成的方法。你可以在启用排序之前使用 `distance` 属性设置一个触发拖动的最小距离(单位是像素)。也可以在启用排序之前使用 `pressDelay` 增加一个触发拖动的延迟。或者你也可以使用[HandleDirective](https://github.com/Jexordexan/vue-slicksort/blob/main/src/HandleDirective.js)
402 |
403 | ### Scoped styles
404 |
405 | 如果你在可排序列表中使用 scoped styles ,你可以使用 `appendTo` 属性。
406 |
407 | ## Dependencies
408 |
409 | Slicksort 无依赖。`vue` 是仅有的前置依赖。
410 |
411 | ## Example
412 |
413 | 这里有一个 `vue-slicksort` 的 [demo 项目](https://github.com/qiqihaobenben/vue-slicksort-demo),可以 down 到本地跑起来,把上面介绍的属性和事件实际用一下。
414 |
415 | ## Reporting Issues
416 |
417 | 如果你觉得发现了问题,请 [报告该问题](https://github.com/Jexordexan/vue-slicksort/issues) 以及所有相关详细信息以重现该问题。 提 issue 最简单的方法是 fork 此 [jsfiddle](https://jsfiddle.net/Jexordexan/1puv2L6c/) 。
418 |
419 | ## Asking for help
420 |
421 | 如果是个性化的支持请求,请用 `question` 标签标记 issue。
422 |
423 | ## Contributions
424 |
425 | 欢迎提交 Feature requests / pull requests 。
426 |
427 | ## Thanks
428 |
429 | 这个库很大程度上是基于 Claudéric Demers (@clauderic) 的 [react-sortable-hoc](https://github.com/clauderic/react-sortable-hoc) ,它是一个表现非常好的简单且高效的拖拽库。
430 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/DragHandle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/FruitExample.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
17 |
18 |
19 | v-model
20 |
21 |
fruits: {{ fruits }}
22 |
23 |
24 |
25 |
26 |
49 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/GroupExample.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Winner!
6 | Reset
7 |
8 |
9 |
10 |
{{ list.name }}
11 | group: '{{ list.group }}'
12 |
13 | accept: {{ list.accept }}
14 |
23 |
29 |
30 |
31 |
32 |
33 | v-models
34 |
35 |
lists: {{ lists }}
36 |
37 |
38 |
39 |
40 |
154 |
155 |
179 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/KanbanExample.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 | {{ col.name }}
15 | ({{ col.items.length }})
16 |
17 |
24 |
30 |
31 | {{ item.value }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
93 |
94 |
140 |
141 |
163 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/LongListExample.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
32 |
33 |
39 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/PageListExample.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
32 |
33 |
39 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/ShorthandExample.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ item }}
5 |
6 |
7 |
8 | v-model
9 |
10 |
fruits: {{ fruits }}
11 |
12 |
13 |
14 |
15 |
29 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/SimpleGroupExample.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ shelf.name }}
5 | group: 'groceries'
6 |
7 |
8 |
9 |
10 |
11 |
{{ cart.name }}
12 | group: 'groceries'
13 |
14 |
15 |
16 |
17 |
18 |
19 | v-models
20 |
21 |
22 |
23 |
shelf: {{ shelf }}
24 |
25 |
26 |
27 |
28 |
cart: {{ cart }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
88 |
89 |
99 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/SortableItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | {{ item.value }}
8 | Add to cart
9 |
10 |
11 |
12 |
30 |
31 |
36 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/SortableList.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/utils.js:
--------------------------------------------------------------------------------
1 | export const random = (a = 1, b = 0) => {
2 | const lower = Math.min(a, b);
3 | const upper = Math.max(a, b) + 1;
4 | return lower + Math.floor(Math.random() * (upper - lower));
5 | };
6 |
7 | export const range = (length) => Array.from({ length }, (_, i) => i);
8 |
9 | export const uid = () => {
10 | // Math.random should be unique because of its seeding algorithm.
11 | // Convert it to base 36 (numbers + letters), and grab the first 9 characters
12 | // after the decimal.
13 | return '_' + Math.random().toString(36).substr(2, 9);
14 | };
15 |
16 | export const stringsToItems = (arr) => {
17 | return arr.map((item) => {
18 | return {
19 | id: uid(),
20 | value: item,
21 | };
22 | });
23 | };
24 |
25 | export const track = (...args) => {
26 | if (typeof gtag !== 'undefined') {
27 | gtag(...args);
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress'
2 |
3 | const GOOGLE_APP_ID = 'G-6JF11BVDSJ'
4 |
5 | /*
6 |
7 |
14 | */
15 |
16 | export default defineConfig({
17 | title: 'Vue Slicksort',
18 | cleanUrls: true,
19 | head: [
20 | ['link', { rel: 'icon', sizes: '32x32', href: '/favicon-32x32.png' }],
21 | ['link', { rel: 'icon', sizes: '16x16', href: '/favicon-16x16.png' }],
22 | ['script', { src: '/confetti.min.js' }],
23 | ['script', { src: `https://www.googletagmanager.com/gtag/js?id=${GOOGLE_APP_ID}` }],
24 | process.env.NODE_ENV === 'production'
25 | ? [
26 | 'script',
27 | {},
28 | `
29 | window.dataLayer = window.dataLayer || [];
30 | function gtag() {dataLayer.push(arguments);}
31 | gtag('js', new Date());
32 | gtag('config', '${GOOGLE_APP_ID}');
33 | gtag('create', '${GOOGLE_APP_ID}', 'auto');
34 | gtag('set', 'anonymizeIp', true);
35 | `,
36 | ]
37 | : ['meta', {}],
38 | ['meta', { property: 'og:title', content: 'Vue Slicksort' }],
39 | ['meta', { property: 'og:image', content: '/logo.png' }],
40 | ['meta', { property: 'og:image:width', content: '375' }],
41 | ['meta', { property: 'og:image:height', content: '375' }],
42 | ['meta', { property: 'og:image', content: '/logomark.png' }],
43 | ['meta', { property: 'og:image:width', content: '1219' }],
44 | ['meta', { property: 'og:image:height', content: '301' }],
45 | [
46 | 'meta',
47 | { property: 'og:description', content: 'Beautiful, touch-friendly sorting for Vue 3' },
48 | ],
49 | ['meta', { property: 'og:url', content: 'https://vue-slicksort.netlify.app' }],
50 | ['meta', { property: 'og:locale', content: 'en_US' }],
51 | ['meta', { property: 'twitter:image', content: '/logo.png' }],
52 | ['meta', { property: 'twitter:title', content: 'Vue Slicksort' }],
53 | ],
54 | themeConfig: {
55 | logo: '/logo.svg',
56 | socialLinks: [{ icon: 'github', link: 'https://github.com/Jexordexan/vue-slicksort' }],
57 | nav: [
58 | { text: 'Guide', link: '/introduction' },
59 | { text: 'Migration from 1.x', link: '/migrating-1x' },
60 | ],
61 |
62 | sidebar: [
63 | {
64 | text: 'Guide',
65 | items: [
66 | { text: 'Introduction', link: '/introduction' },
67 | { text: 'Getting started', link: '/getting-started' },
68 | { text: 'Basic use', link: '/basics' },
69 | { text: 'Drag and drop', link: '/drag-and-drop' },
70 | { text: 'Troubleshooting', link: '/troubleshooting' },
71 | { text: 'Migrating from 1.x', link: '/migrating-1x' },
72 | ],
73 | },
74 | {
75 | text: 'Components',
76 | items: [
77 | { text: 'SlickList', link: '/components/slicklist' },
78 | { text: 'SlickItem', link: '/components/slickitem' },
79 | { text: 'DragHandle', link: '/components/draghandle' },
80 | ],
81 | },
82 | {
83 | text: 'Demos',
84 | items: [
85 | { text: 'Kanban', link: '/kanban' },
86 | { text: 'Window scroll', link: '/window-scroll' },
87 | ],
88 | },
89 | ],
90 | },
91 | })
92 |
--------------------------------------------------------------------------------
/docs/.vitepress/style.styl:
--------------------------------------------------------------------------------
1 | $rubyred = #eb5757
2 | $purp = #9b51e0
3 | $bluetopaz = #58cbf2
4 |
5 | :root
6 | --vp-c-brand-lighter: lighten($purp, 30%);
7 | --vp-c-brand-light: lighten($purp, 20%);
8 | --vp-c-brand: $purp;
9 | --vp-c-brand-dark: darken($purp, 0%);
10 | --vp-c-brand-darker: darken($purp, 10%);
11 |
12 | .nav-bar-title .logo
13 | height: 2rem;
14 |
15 | ul.example-list
16 | margin 0
17 | padding 0
18 | overflow auto
19 | &.horizontal
20 | display flex
21 |
22 | .example-list-item
23 | list-style-type none
24 | display flex
25 | align-items center
26 | padding 10px 20px
27 | margin 10px
28 | border-radius 10px
29 | font-weight bold
30 | background white
31 | box-shadow inset 0 0 0 3px rgba(0, 0, 0, 0.1), 1px 2px 5px rgba(0, 0, 0, 0.15)
32 | background $purp
33 | color white
34 | line-height 1.4
35 | user-select none
36 |
37 | // Drag handle
38 | .example-drag-handle
39 | display inline-block
40 | width 25px
41 | height 25px
42 | background-image url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 50 50'%3E%3Cpath d='M 0 7.5 L 0 12.5 L 50 12.5 L 50 7.5 L 0 7.5 z M 0 22.5 L 0 27.5 L 50 27.5 L 50 22.5 L 0 22.5 z M 0 37.5 L 0 42.5 L 50 42.5 L 50 37.5 L 0 37.5 z' color='white'%3E%3C/path%3E%3C/svg%3E")
43 | background-size contain
44 | background-repeat no-repeat
45 | opacity 0.5
46 | margin-right 10px
47 | cursor: grab
48 |
49 | h4
50 | margin-top 1em
51 |
52 |
53 | .doc-button
54 | padding: 5px 15px;
55 | background: white;
56 | color: var(--vp-c-brand);
57 | border: 2px solid #ccc;
58 | border-radius: 5px;
59 | cursor: pointer;
60 | font-size: inherit;
61 |
62 | &:hover
63 | background: #eee;
64 |
65 | &:active
66 | background: #ccc;
67 |
68 | &.space-start
69 | margin-inline-start: auto;
70 |
71 | .kanban-page
72 | max-width: 0
73 |
74 | .inline-badge
75 | > p
76 | display flex
77 | gap 1rem
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.js:
--------------------------------------------------------------------------------
1 | import { watchEffect } from 'vue';
2 | import DefaultTheme from 'vitepress/theme';
3 | import GroupExample from '../components/GroupExample.vue';
4 | import FruitExample from '../components/FruitExample.vue';
5 | import LongListExample from '../components/LongListExample.vue';
6 | import PageListExample from '../components/PageListExample.vue';
7 | import ShorthandExample from '../components/ShorthandExample.vue';
8 | import SimpleGroupExample from '../components/SimpleGroupExample.vue';
9 | import KanbanExample from '../components/KanbanExample.vue';
10 | import { track } from '../components/utils';
11 | import { plugin } from '../../../src';
12 | import '../style.styl';
13 |
14 | export default {
15 | ...DefaultTheme,
16 | enhanceApp({ app, router }) {
17 | watchEffect(() => {
18 | track('set', 'page', router.route.path);
19 | track('send', 'pageview');
20 | });
21 |
22 | app.use(plugin);
23 | app.component('GroupExample', GroupExample);
24 | app.component('FruitExample', FruitExample);
25 | app.component('ShorthandExample', ShorthandExample);
26 | app.component('LongListExample', LongListExample);
27 | app.component('PageListExample', PageListExample);
28 | app.component('SimpleGroupExample', SimpleGroupExample);
29 | app.component('KanbanExample', KanbanExample);
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/docs/basics.md:
--------------------------------------------------------------------------------
1 | # Basic Use
2 |
3 | This is a basic example of a sortable list. `v-model:list` allows data-binding between the list of fruits.
4 |
5 | ```vue
6 |
7 |
8 |
9 | {{ fruit }}
10 |
11 |
12 |
13 |
14 |
29 | ```
30 |
31 | **Try it out!**
32 |
33 |
34 |
35 |
36 | ::: tip
37 | `:key` should refer to unique data, like an ID attribute, that can be used to track the item as it moves. Using the index (`i`) as the key is not advised and may cause unwanted rendering effects.
38 | :::
39 |
40 | ## Slot Shorthand
41 |
42 | If you want build sortable lists quickly, use the `item` scoped slot in the `SlickList` component.
43 | This slot will repeat for each item in the `list` prop. This has to be used with `v-model:list` or the `:list` prop on the container.
44 |
45 | ```vue
46 |
47 |
48 |
49 |
50 | {{ item }}
51 |
52 |
53 |
54 | ```
55 |
56 |
57 |
58 |
59 |
60 | ## Horizontal List
61 |
62 | If your lists are layed out along the X axis, pass in the `axis="x"` prop and it will just work!
63 | You will have to handle the actual CSS of making them horizontal, `display: flex` should do the trick.
64 |
65 | ```html
66 |
67 |
68 |
69 | ```
70 |
71 |
72 |
73 |
74 |
75 | ## Using a drag handle
76 |
77 | By default the entire list item is the handle for the drag events. This might be a problem if you have inputs or buttons or other interactive elements within the items. You can use the drag handle mixin to make any element a handle.
78 |
79 | ```html
80 |
81 |
82 |
83 |
84 |
85 | {{ fruit }}
86 |
87 |
88 | ```
89 |
90 |
91 |
92 |
93 |
94 | ## Autoscroll
95 |
96 | Slicksort will automatically autoscroll within the container if it overflows.
97 |
98 | ```vue
99 |
100 |
101 |
102 |
103 | {{ item }}
104 |
105 |
106 |
107 |
108 |
109 |
125 |
126 |
132 | ```
133 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/docs/components/draghandle.md:
--------------------------------------------------------------------------------
1 |
2 | # `DragHandle` Component
3 |
4 | This component becomes a dedicated handle for the SlickItem. The SlickList must have the `useDragHandle` prop.
5 |
6 | ## Props
7 |
8 | ### `tag`
9 |
10 | type: `string`
11 |
12 | default: `div`
13 |
14 | The HTML tag that will render in the DOM.
15 |
16 | ## Slots
17 |
18 | ### `default`
19 |
20 | The item content
21 |
22 |
--------------------------------------------------------------------------------
/docs/components/slickitem.md:
--------------------------------------------------------------------------------
1 |
2 | # `SlickItem` Component
3 |
4 | ## Props
5 |
6 | ### `index` _(required)_
7 |
8 | type: `number`
9 |
10 | **Required**
11 |
12 | This is the element's sortableIndex within it's collection. This prop is required.
13 |
14 | ### `collection`
15 |
16 | **REMOVED IN v2.0.0**
17 | Use `group` and multiple scroll containers instead.
18 |
19 | See [Migration docs](/migrating-1x.html#collection-removed)
20 |
21 | ### `disabled`
22 |
23 | type: `boolean`
24 |
25 | default: `false`
26 |
27 | Whether the element should be sortable or not
28 |
29 | ### `tag`
30 |
31 | type: `string`
32 |
33 | default: `div`
34 |
35 | The HTML tag that will render in the DOM.
36 |
37 | ## Slots
38 |
39 | ### `default`
40 |
41 | The item content
42 |
--------------------------------------------------------------------------------
/docs/components/slicklist.md:
--------------------------------------------------------------------------------
1 |
2 | # `SlickList` Component
3 |
4 | The `SlickList` contains several [`SlickItem`](slickitem)
5 |
6 | ## Props
7 |
8 | ### `tag`
9 |
10 | type: `string`
11 |
12 | default: `div`
13 |
14 | The HTML tag that will render in the DOM.
15 |
16 | ### `list`
17 |
18 | type: `any[]`
19 |
20 | **Required**
21 |
22 | The `list` can be inherited from `v-model:list` but has to be set to the same list that is rendered with `v-for` inside the `SlickList`.
23 |
24 | ### `group`
25 |
26 | type: `string`
27 |
28 | The group that this list belongs to. By default, the group is not set and items cannot be dragged in or out of the list. When the group is set, all lists in that group can drag between each other.
29 |
30 | ::: warning
31 | Setting the `group` prop requires the `Slicksort` vue plugin to be installed in your app.
32 | :::
33 |
34 | ### `accept`
35 |
36 | type: `true | string[] | ({ source, dest, payload }) => boolean`
37 |
38 | default: `null`
39 |
40 | The groups that can be dragged into this container. If `true`, the list will accept items from all other lists. If `string[]`, the list will accept groups listed. If a function is passed, it should return a boolean, where `true` means the item is acceptable. The function will be called with a context object with the following properties:
41 |
42 | - `source`: The drag source component, with properties like `group`, `list`, and others
43 | - `dest`: The drag destination component, with the same properties as the `source`
44 | - `payload`: the data value of the item that is being dragged
45 |
46 | ### `block`
47 |
48 | type: `string[]`
49 |
50 | default: `[]`
51 |
52 | Allows you to block specific groups. This will override `accept: true`, to allow all but a few groups.
53 |
54 | ### `axis`
55 |
56 | type: `string`
57 |
58 | default: `y`
59 |
60 | Items can be sorted horizontally, vertically or in a grid. Possible values: `x`, `y` or `xy`
61 |
62 | ### `lockAxis`
63 |
64 | type: `string`
65 |
66 | If you'd like, you can lock movement to an axis while sorting. This is not something that is possible with HTML5 Drag & Drop
67 |
68 | ### `helperClass`
69 |
70 | type: `string`
71 |
72 | You can provide a class you'd like to add to the sortable helper to add some styles to it
73 |
74 | ### `appendTo`
75 |
76 | type: `string` (Query selector)
77 |
78 | default: `body`
79 |
80 | This is the element that the sortable helper is added to when sorting begins. You would change this if you would like to encapsulate the drag helper within a positioned or scrolled container.
81 |
82 | ### `transitionDuration`
83 |
84 | type: `number`
85 |
86 | default: `300`
87 |
88 | The duration of the transition when elements shift positions. Set this to `0` if you'd like to disable transitions
89 |
90 | ### `draggedSettlingDuration`
91 |
92 | type: `number`
93 |
94 | default: `transitionDuration`
95 |
96 | Override the settling duration for the drag helper. If not set, `transitionDuration` will be used.
97 |
98 | ### `pressDelay`
99 |
100 | type: `number`
101 |
102 | default: `0`
103 |
104 | If you'd like elements to only become sortable after being pressed for a certain time, change this property. A good sensible default value for mobile is `200`. Cannot be used in conjunction with the `distance` prop.
105 |
106 | ### `pressThreshold`
107 |
108 | type: `number`
109 |
110 | default: `5`
111 |
112 | Number of pixels of movement to tolerate before ignoring a press event.
113 |
114 | ### `distance`
115 |
116 | type: `number`
117 |
118 | default: `0`
119 |
120 | If you'd like elements to only become sortable after being dragged a certain number of pixels. Cannot be used in conjunction with the `pressDelay` prop.
121 |
122 | ### `cancelKey`
123 |
124 | type: `string`
125 |
126 | default: `"Escape"`
127 |
128 | The key that will stop the current drag and return the element to its original position. Fires `@sort-cancel` when complete. This can be any [KeyboardEvent.key value](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values).
129 |
130 | ### `useDragHandle`
131 |
132 | type: `boolean`
133 |
134 | default: `false`
135 |
136 | If you're using the `HandleDirective`, set this to `true`
137 |
138 | ### `useWindowAsScrollContainer`
139 |
140 | type: `boolean`
141 |
142 | default: `false`
143 |
144 | If you want, you can set the `window` as the scrolling container
145 |
146 | ### `hideSortableGhost`
147 |
148 | type: `boolean`
149 |
150 | default: `true`
151 |
152 | Whether to auto-hide the ghost element. By default, as a convenience, Vue Slicksort List will automatically hide the element that is currently being sorted. Set this to false if you would like to apply your own styling.
153 |
154 | ### `lockToContainerEdges`
155 |
156 | type: `boolean`
157 |
158 | default: `false`
159 |
160 | You can lock movement of the sortable element to it's parent `Container`
161 |
162 | ### `lockOffset`
163 |
164 | type: _`OffsetValue` or [ `OffsetValue`, `OffsetValue` ]_\*
165 |
166 | default: `"50%"`
167 |
168 | When `lockToContainerEdges` is set to `true`, this controls the offset distance between the sortable helper and the top/bottom edges of it's parent `Container`. Percentage values are relative to the height of the item currently being sorted. If you wish to specify different behaviours for locking to the _top_ of the container vs the _bottom_, you may also pass in an `array` (For example: `["0%", "100%"]`).
169 |
170 | \* `OffsetValue` can either be a finite `Number` or a `String` made up of a number and a unit (`px` or `%`).
171 | Examples: `10` (which is the same as `"10px"`), `"50%"`
172 |
173 | ### `shouldCancelStart`
174 |
175 | type: _Function_
176 |
177 | default: [Function](https://github.com/Jexordexan/vue-slicksort/blob/master/src/ContainerMixin.js#L41)
178 |
179 | This function is invoked before sorting begins, and can be used to programatically cancel sorting before it begins. By default, it will cancel sorting if the event target is either an `input`, `textarea`, `select` or `option`.
180 |
181 | ### `getHelperDimensions`
182 |
183 | type: _Function_
184 |
185 | default: [Function](https://github.com/Jexordexan/vue-slicksort/blob/master/src/ContainerMixin.js#L49)
186 |
187 | Optional `function({node, index})` that should return the computed dimensions of the SortableHelper. See [default implementation](https://github.com/Jexordexan/vue-slicksort/blob/master/src/ContainerMixin.js#L49) for more details
188 |
189 | ## Events
190 |
191 | Events are emitted from the Container element, and can be bound to using `v-bind` or `@` directives
192 |
193 | ### `@sort-start`
194 |
195 | emits: `{ event: MouseEvent, node: HTMLElement, index: number }`
196 |
197 | Fired when sorting begins.
198 |
199 | ### `@sort-move`
200 |
201 | emits: `{ event }`
202 |
203 | Fired when the mouse is moved during sorting.
204 |
205 | ### `@sort-end`
206 |
207 | emits: `{ event, newIndex, oldIndex }`
208 |
209 | Fired when sorting has ended.
210 |
211 | ### `@sort-cancel`
212 |
213 | emits: `{ event, newIndex, oldIndex }`
214 |
215 | Fired when sorting has been canceled. YOu can set which key cancels a drag with the `cancelKey` prop.
216 |
217 | ### `@sort-insert`
218 |
219 | emits: `{ newIndex, value }`
220 |
221 | Fired when an item is dragged from another list into this one
222 |
223 | ### `@sort-remove`
224 |
225 | emits: `{ oldIndex }`
226 |
227 | Fired when an item is dragged from this list and dropped in another
228 |
229 | ### `@update:list`
230 |
231 | emits: `Array`
232 |
233 | Fired after sorting has ended with the newly sorted list. This is compatible with `v-model:list`.
234 |
235 | ## Slots
236 |
237 | ### `default`
238 |
239 | scope: `none`
240 |
241 | The Your list of `SlickItems` goes here
242 |
243 | ### `item` (scoped)
244 |
245 | scope: `{ item }`
246 |
247 | This is a scoped slot that will repeat for every item in `list`. This should be used in place of the `default` slot for making easier sortable list.
248 |
249 |
--------------------------------------------------------------------------------
/docs/drag-and-drop.md:
--------------------------------------------------------------------------------
1 |
2 | # Drag and drop
3 |
4 | New in V2, you can now drag items between lists!
5 |
6 | In order to enabled drag-and-drop, you need to install the Slicksort plugin. Instructions are on the [Getting started](/getting-started.html#using-the-plugin) page.
7 |
8 | Every list that you want to drag between then needs the `group` prop to be set. If all lists have the same "group", they will allow dragging between all lists. If using different groups, you can use the `accept` prop to allow a set of other groups that can be dragged into the list.
9 |
10 | ```html
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ```
23 |
24 | ## Between the same group
25 |
26 |
27 |
28 |
29 |
30 | ## Between different groups
31 |
32 | In the example below, three lists have different groups and `accept` props.
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting started
2 |
3 | ## Installing
4 |
5 | Install the package to your project
6 |
7 | ```
8 | npm install vue-slicksort@next
9 | ```
10 |
11 | ```
12 | yarn add vue-slicksort@next
13 | ```
14 |
15 | ## Using the plugin
16 |
17 | If you want the ability to drag items between lists, you must install the plugin to your Vue app.
18 |
19 | ```js
20 | // main.js
21 |
22 | import { plugin as Slicksort } from 'vue-slicksort';
23 |
24 | const app = createApp(/* App */);
25 |
26 | // Enables groups and drag and drop functionality
27 | app.use(Slicksort);
28 | ```
29 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 |
4 | hero:
5 | name: Vue Slicksort
6 | # text: Super slick
7 | tagline: Super smooth, touch-friendly, sorting for Vue 3
8 | image:
9 | src: /Logo.svg
10 | alt: Slicksort
11 | actions:
12 | - theme: brand
13 | text: To the Docs!
14 | link: /introduction
15 | - theme: alt
16 | text: View on GitHub
17 | link: https://github.com/jexordexan/vue-slicksort
18 | features:
19 | - title: 🥰 Simple
20 | details: Easy sorting with `v-model` components. Drag and drop between lists is also supported!
21 | - title: ⚡️ Fast
22 | details: Silky smooth animations at 60 fps 🔥. Users will be delighted by the experience of sorting
23 | - title: 🪶 Light
24 | details: Only 7kb (gzipped) and no dependencies!
25 | footer: MIT Licensed | Copyright © 2019-present Jordan Simonds
26 | ---
27 |
28 |
--------------------------------------------------------------------------------
/docs/introduction.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | Slicksort exports two components for quickly creating sortable lists. These have to be used together, or with the slot shorthand.
4 |
5 | - `SlickList` For wrapping a list of items ([API](components/slicklist))
6 | - `SlickItem` For wrapping a single item and its content ([API](components/slickitem))
7 |
8 | The functionality of these components are also available as mixins, by using the `ContainerMixin` and `ElementMixin`, you can make your own sortable components.
9 |
10 |
11 |
12 | [](https://www.npmjs.com/package/vue-slicksort)
13 | [](https://www.npmjs.com/package/vue-slicksort)
14 | [](https://bundlephobia.com/result?p=vue-slicksort@next)
15 | [](https://github.com/Jexordexan/vue-slicksort/blob/dev/LICENSE)
16 |
17 |
18 |
19 | ## Try it out!
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/docs/kanban.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Kanban example'
3 | aside: false
4 | ---
5 |
6 | ## Kanban example
7 |
8 | This example uses many advanced features at once. You can see the source code [here](https://github.com/Jexordexan/vue-slicksort/blob/docs/docs/.vitepress/components/KanbanExample.vue).
9 | - Nested lists
10 | - Drag and drop
11 | - Grid sort
12 | - Lock axis
13 | - Drag handle
14 | - Helper styles
15 |
16 |
17 |
--------------------------------------------------------------------------------
/docs/migrating-1x.md:
--------------------------------------------------------------------------------
1 | # Migrating from 1.x
2 |
3 | Slicksort Version 2 is an overhaul of Version 1 with many awaited features, including sorting between lists! It also comes with some breaking changes.
4 |
5 | ## Breaking changes
6 |
7 | ### Vue 3 only
8 |
9 | Vue 2 support has been removed for now. We may revisit this later with the [vue-demi](https://github.com/vueuse/vue-demi) library. In order to use v2 of `vue-slicksort` you must upgrade to Vue 3. Apologies for the inconvenience!
10 |
11 | ### V-model changes
12 |
13 | With the transition to Vue 3, we chose to use named v-model. Anywhere you used `v-model` in V1, must become `v-model:list`.
14 |
15 | Before:
16 |
17 | ```
18 | ...
19 | ```
20 |
21 | After:
22 |
23 | ```
24 | ...
25 | ```
26 |
27 | This was done to make the emitted events more explicit, so if you were using the expanded v-model syntax, the props/events have changed from `:value` and `@input` to `:list` and `@update:list`.
28 |
29 | Before:
30 |
31 | ```
32 | ...
33 | ```
34 |
35 | After:
36 |
37 | ```
38 | ...
39 | ```
40 |
41 | ### `collection` Removed
42 |
43 | The `collection` prop has been removed from the `Element`. This props purpose has been replaced by the `group` prop on the `Container`, which can allow/prevent dragging between lists. This means that any list with elements of different `collections` should be refactored to be a set of lists with different groups. This is a simple example of the transition.
44 |
45 | **Before**
46 |
47 | ```html
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | ```
56 |
57 | **After**
58 |
59 | ```html
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | ```
72 |
73 |
105 |
--------------------------------------------------------------------------------
/docs/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/docs/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/docs/public/android-chrome-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/docs/public/android-chrome-256x256.png
--------------------------------------------------------------------------------
/docs/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/docs/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/docs/public/confetti.min.js:
--------------------------------------------------------------------------------
1 | var confetti={maxCount:150,speed:2,frameInterval:15,alpha:1,gradient:!1,start:null,stop:null,toggle:null,pause:null,resume:null,togglePause:null,remove:null,isPaused:null,isRunning:null};!function(){confetti.start=s,confetti.stop=w,confetti.toggle=function(){e?w():s()},confetti.pause=u,confetti.resume=m,confetti.togglePause=function(){i?m():u()},confetti.isPaused=function(){return i},confetti.remove=function(){stop(),i=!1,a=[]},confetti.isRunning=function(){return e};var t=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame,n=["rgba(30,144,255,","rgba(107,142,35,","rgba(255,215,0,","rgba(255,192,203,","rgba(106,90,205,","rgba(173,216,230,","rgba(238,130,238,","rgba(152,251,152,","rgba(70,130,180,","rgba(244,164,96,","rgba(210,105,30,","rgba(220,20,60,"],e=!1,i=!1,o=Date.now(),a=[],r=0,l=null;function d(t,e,i){return t.color=n[Math.random()*n.length|0]+(confetti.alpha+")"),t.color2=n[Math.random()*n.length|0]+(confetti.alpha+")"),t.x=Math.random()*e,t.y=Math.random()*i-i,t.diameter=10*Math.random()+5,t.tilt=10*Math.random()-10,t.tiltAngleIncrement=.07*Math.random()+.05,t.tiltAngle=Math.random()*Math.PI,t}function u(){i=!0}function m(){i=!1,c()}function c(){if(!i)if(0===a.length)l.clearRect(0,0,window.innerWidth,window.innerHeight),null;else{var n=Date.now(),u=n-o;(!t||u>confetti.frameInterval)&&(l.clearRect(0,0,window.innerWidth,window.innerHeight),function(){var t,n=window.innerWidth,i=window.innerHeight;r+=.01;for(var o=0;on+20||t.x<-20||t.y>i)&&(e&&a.length<=confetti.maxCount?d(t,n,i):(a.splice(o,1),o--))}(),function(t){for(var n,e,i,o,r=0;ro){var f=n;n=o,o=f}s=a.length+(Math.random()*(o-n)+n|0)}else s=a.length+n;else o&&(s=a.length+o);for(;a.length
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/public/logomark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/docs/public/logomark.png
--------------------------------------------------------------------------------
/docs/troubleshooting.md:
--------------------------------------------------------------------------------
1 | # Troubleshooting
2 |
3 | ## FAQ
4 |
5 | ### Items re-render after sort
6 |
7 | You need to use the `:key` attribute on every list item, and it needs to be unique to the object you are displaying, like an `id`. Using `index` can lead to re-rendered lists because the index changes when sorting
8 |
9 | ✅ **GOOD**
10 |
11 | ```html
12 |
13 | ```
14 |
15 | ❌ **BAD**
16 |
17 | ```html
18 |
19 | ```
20 |
21 | ### Use with `transition-group`
22 |
23 | Slicksort is not compatible with `transition-group` because they both try to animate the positions of the nodes. Once the drag is done and the `list` is updated, `transition-group` then tries to animate everything again.
24 |
25 | ### Drag helper is unstyled
26 |
27 | Since the drag helper is appended to the `body`, any scoped styles will not apply. This could be the situation if your css relies on:
28 |
29 | - `
52 |
--------------------------------------------------------------------------------
/example/components/GroupExample.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ list.name }}
5 | Accepts: {{ list.accept }}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/example/components/InnerList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ list.name }}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/example/components/SortableItem.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ item.value }}
3 |
4 |
5 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/example/components/SortableList.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Vue Sortable
5 |
6 |
51 |
52 |
53 | Loading demo
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import { plugin as Slicksort } from '../src';
3 | import Example from './Example.vue';
4 |
5 | const app = createApp(Example);
6 |
7 | app.use(Slicksort);
8 |
9 | app.mount('#root');
10 |
--------------------------------------------------------------------------------
/example/util/index.js:
--------------------------------------------------------------------------------
1 | export function random(min, max) {
2 | return Math.floor(Math.random() * (max - min)) + min;
3 | }
4 |
5 | export function range(length) {
6 | return Array(length)
7 | .fill()
8 | .map((_, i) => i);
9 | }
10 |
--------------------------------------------------------------------------------
/example/vite.config.js:
--------------------------------------------------------------------------------
1 | import vue from '@vitejs/plugin-vue';
2 |
3 | export default {
4 | plugins: [vue()],
5 | };
6 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'vue-slicksort' {
2 | import { DirectiveOptions, Component, Plugin } from 'vue';
3 |
4 | export const ContainerMixin: Component;
5 | export const ElementMixin: Component;
6 |
7 | export const SlickList: Component;
8 | export const SlickItem: Component;
9 | export const DragHandle: Component;
10 |
11 | export const HandleDirective: DirectiveOptions;
12 |
13 | export function arrayMove(arr: Array, prevIndex: number, newIndex: number): Array;
14 |
15 | export const plugin: Plugin;
16 | }
17 |
--------------------------------------------------------------------------------
/logo/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/logo/android-chrome-192x192.png
--------------------------------------------------------------------------------
/logo/android-chrome-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/logo/android-chrome-256x256.png
--------------------------------------------------------------------------------
/logo/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/logo/apple-touch-icon.png
--------------------------------------------------------------------------------
/logo/brand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/logo/brand.png
--------------------------------------------------------------------------------
/logo/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/logo/demo.gif
--------------------------------------------------------------------------------
/logo/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/logo/favicon-16x16.png
--------------------------------------------------------------------------------
/logo/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/logo/favicon-32x32.png
--------------------------------------------------------------------------------
/logo/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/logo/favicon.ico
--------------------------------------------------------------------------------
/logo/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/logo/logo.png
--------------------------------------------------------------------------------
/logo/logomark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jexordexan/vue-slicksort/7a8fee6bc8aa8d3e3c4b33468c3429621b16046a/logo/logomark.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-slicksort",
3 | "version": "2.0.5",
4 | "description": "Set of mixins to turn any list into a sortable, touch-friendly, animated list",
5 | "author": {
6 | "name": "Jordan Simonds",
7 | "email": "simonds.jordan@gmail.com"
8 | },
9 | "user": "jsimonds",
10 | "homepage": "https://vue-slicksort.netlify.app",
11 | "main": "dist/vue-slicksort.cjs.js",
12 | "module": "dist/vue-slicksort.esm.js",
13 | "browser": "dist/vue-slicksort.umd.js",
14 | "unpkg": "dist/vue-slicksort.min.js",
15 | "license": "MIT",
16 | "repository": {
17 | "type": "git",
18 | "url": "https://github.com/Jexordexan/vue-slicksort.git"
19 | },
20 | "bugs": {
21 | "url": "https://github.com/Jexordexan/vue-slicksort/issues"
22 | },
23 | "keywords": [
24 | "vue",
25 | "vuejs",
26 | "vue-component",
27 | "vue-mixin",
28 | "sortable",
29 | "sortable-list",
30 | "list",
31 | "sortable list",
32 | "smooth",
33 | "animated",
34 | "mixin",
35 | "component",
36 | "react-sortable-hoc",
37 | "vue-slicksort",
38 | "vue-sort",
39 | "vue-sortable",
40 | "drag and drop",
41 | "vue-slicksort"
42 | ],
43 | "scripts": {
44 | "start": "vite example",
45 | "changelog": "standard-changelog",
46 | "version": "npm run changelog && git add CHANGELOG.md",
47 | "build": "npm run clean && npm run rollup",
48 | "prepublishOnly": "npm run build",
49 | "rollup": "cross-env BABEL_ENV=rollup rollup -c",
50 | "clean": "rimraf dist/",
51 | "test": "eslint src/** --ext .ts && npm run test:cy",
52 | "cy": "cypress open --component",
53 | "test:cy": "cypress run --component",
54 | "deploy:alpha": "npm test && npm version prerelease --preid=alpha && npm run deploy:final",
55 | "deploy:patch": "npm test && npm version patch && npm run deploy:final",
56 | "deploy:final": "git push; git push --tags; npm publish",
57 | "docs:dev": "vitepress dev docs",
58 | "docs:build": "vitepress build docs",
59 | "docs:serve": "vitepress serve docs"
60 | },
61 | "peerDependencies": {
62 | "vue": ">=3.0.0"
63 | },
64 | "devDependencies": {
65 | "@babel/core": "^7.13.14",
66 | "@babel/plugin-external-helpers": "^7.12.13",
67 | "@babel/plugin-transform-regenerator": "^7.13.15",
68 | "@babel/plugin-transform-runtime": "^7.13.15",
69 | "@babel/preset-env": "^7.12.16",
70 | "@cypress/vite-dev-server": "^4.0.1",
71 | "@cypress/vue": "^5.0.1",
72 | "@rollup/plugin-typescript": "^8.2.0",
73 | "@typescript-eslint/eslint-plugin": "^5.43.0",
74 | "@typescript-eslint/parser": "^5.43.0",
75 | "@vitejs/plugin-vue": "^3.2.0",
76 | "@vitejs/plugin-vue-jsx": "^2.1.1",
77 | "@vue/compiler-sfc": "^3.2.45",
78 | "@vue/test-utils": "^2.0.0-rc.6",
79 | "@vueuse/core": "^9.5.0",
80 | "autoprefixer": "^6.3.6",
81 | "cross-env": "^1.0.7",
82 | "cssnano": "^3.10.0",
83 | "cypress": "^11.1.0",
84 | "esbuild": "^0.8.44",
85 | "eslint": "^7.20.0",
86 | "eslint-plugin-cypress": "^2.11.2",
87 | "eslint-plugin-import": "^2.0.1",
88 | "eslint-plugin-vue": "^7.6.0",
89 | "postcss": "^8.2.6",
90 | "rimraf": "^2.5.2",
91 | "rollup": "^2.38.5",
92 | "rollup-plugin-babel": "^4.4.0",
93 | "rollup-plugin-bundle-size": "^1.0.3",
94 | "rollup-plugin-esbuild": "^2.6.1",
95 | "sass": "^1.32.6",
96 | "standard-changelog": "^2.0.27",
97 | "typescript": "^4.2.3",
98 | "vite": "^3.2.4",
99 | "vitepress": "^1.0.0-alpha.46",
100 | "vue": "^3.2.45",
101 | "vuepress": "^2.0.0-alpha.23"
102 | },
103 | "browserslist": [
104 | "> 1%",
105 | "last 4 versions"
106 | ],
107 | "files": [
108 | "index.d.ts",
109 | "dist/*"
110 | ],
111 | "dependencies": {}
112 | }
113 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import esbuild from 'rollup-plugin-esbuild';
3 | import bundleSize from 'rollup-plugin-bundle-size';
4 | import typescript from '@rollup/plugin-typescript';
5 | import pkg from './package.json';
6 |
7 | const input = './src/index.ts';
8 | const moduleName = 'VueSlicksort';
9 |
10 | export default [
11 | {
12 | input,
13 | output: [
14 | { file: pkg.main, format: 'cjs' },
15 | { file: pkg.module, format: 'es' },
16 | { file: pkg.browser, format: 'umd', name: moduleName },
17 | ],
18 | plugins: [typescript(), babel()],
19 | },
20 | {
21 | input,
22 | output: {
23 | file: pkg.unpkg,
24 | name: moduleName,
25 | format: 'umd',
26 | sourcemap: true,
27 | },
28 | plugins: [
29 | esbuild({
30 | minify: true,
31 | define: {
32 | 'process.env.NODE_ENV': '"production"',
33 | },
34 | }),
35 | babel(),
36 | bundleSize(),
37 | ],
38 | },
39 | ];
40 |
--------------------------------------------------------------------------------
/src/ContainerMixin.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/ban-ts-comment */
2 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
3 | import { defineComponent, PropType } from 'vue';
4 | import Manager, { ItemRef, SortableNode } from './Manager';
5 | import SlicksortHub, { AcceptProp, ContainerRef } from './SlicksortHub';
6 | import {
7 | arrayMove,
8 | arrayRemove,
9 | arrayInsert,
10 | cloneNode,
11 | closest,
12 | commonOffsetParent,
13 | events,
14 | getEdgeOffset,
15 | getElementMargin,
16 | getLockPixelOffsets,
17 | getPointerOffset,
18 | limit,
19 | resetTransform,
20 | XY,
21 | TopLeft,
22 | WidthHeight,
23 | PointEvent,
24 | BottomRight,
25 | isTouch,
26 | setTransform,
27 | hasOwnProperty,
28 | } from './utils';
29 |
30 | type PointEventListener = (e: PointEvent) => unknown;
31 | // eslint-disable-next-line @typescript-eslint/ban-types
32 | const timeout: (cb: Function, dur?: number) => number = setTimeout;
33 | type Timer = number | null;
34 |
35 | interface ComponentProps {
36 | list: unknown[];
37 | axis: 'x' | 'y' | 'xy' | 'yx'; // 'x', 'y', 'xy'
38 | distance: number;
39 | pressDelay: number;
40 | pressThreshold: number;
41 | useDragHandle: boolean;
42 | useWindowAsScrollContainer: boolean;
43 | hideSortableGhost: boolean;
44 | lockToContainerEdges: boolean;
45 | lockOffset: string | number | string[];
46 | transitionDuration: number;
47 | appendTo: string;
48 | draggedSettlingDuration: number;
49 | group: string;
50 | accept: boolean | string[] | ((ctx: { source: ContainerRef; dest: ContainerRef; payload: unknown }) => boolean);
51 | cancelKey: string;
52 | block: string[];
53 | lockAxis: string;
54 | helperClass: string;
55 | contentWindow: Window;
56 | shouldCancelStart: (e: PointEvent) => boolean;
57 | getHelperDimensions: (ref: ItemRef) => { width: number; height: number };
58 | }
59 |
60 | interface ComponentData extends ComponentProps {
61 | id: string;
62 |
63 | // usually thi.$el
64 | container: HTMLElement;
65 |
66 | // ref to document
67 | document: Document;
68 |
69 | // Provided for sortable elements to register
70 | manager: Manager;
71 |
72 | // ref to window
73 | _window: Window;
74 |
75 | // window or container
76 | // TODO make this a prop
77 | scrollContainer: { scrollTop: number; scrollLeft: number };
78 |
79 | // Injected and used for drag and drop between lists
80 | hub?: SlicksortHub;
81 |
82 | // Normalized events for mouse and touch devices
83 | events: Record;
84 |
85 | // MOusedown or touchstart occurred
86 | _touched: boolean;
87 |
88 | // initial point of contact
89 | _pos: XY;
90 |
91 | // distance from _pos to current pointer position
92 | // Used for drag threshold, aka `distance`
93 | _delta: XY;
94 |
95 | // Data structure for `axis` prop
96 | _axis: { x: boolean; y: boolean };
97 |
98 | // timer for press threshold
99 | pressTimer: Timer;
100 |
101 | // used to wait until next tick to cancel
102 | cancelTimer: Timer;
103 |
104 | // dragout transition timer
105 | dragendTimer: Timer;
106 |
107 | // Used for repeating autoscroll
108 | autoscrollInterval: Timer;
109 |
110 | // The translation applied to the helper
111 | translate: XY;
112 |
113 | // The minmax values of translate before starting autoscroll
114 | minTranslate: Partial;
115 | maxTranslate: Partial;
116 |
117 | // Is the user currently sorting
118 | sorting: boolean;
119 |
120 | // The user has initiated a cancel action
121 | canceling: boolean;
122 |
123 | // The active node that was originally clicked
124 | node: SortableNode;
125 |
126 | // The measurements of the node that have to be calculated separately
127 | margin: TopLeft & BottomRight;
128 | width: number;
129 | height: number;
130 |
131 | // space added around the active node
132 | marginOffset: XY;
133 |
134 | // Initial offset of the cursor
135 | initialOffset: XY;
136 |
137 | // Initial scroll position of the container and window
138 | initialScroll: TopLeft;
139 | initialWindowScroll: TopLeft;
140 |
141 | // The top-left offset of the node from the offsetParent of the *container*
142 | // Its important to use the container as a source for the offsetParent so all nodes have the same reference point
143 | offsetEdge: TopLeft;
144 |
145 | // client rect for the node and container
146 | boundingClientRect: ClientRect;
147 | containerBoundingRect: ClientRect;
148 |
149 | // Starting index (use last index for drag in operations)
150 | index: number | null;
151 |
152 | // Target index for dropping
153 | newIndex: number | null;
154 |
155 | // The node that follows the mouse
156 | helper: HTMLElement | null;
157 |
158 | // The node that stays in the list and is hidden
159 | sortableGhost: HTMLElement | null;
160 |
161 | // The element that we listen to for events
162 | listenerNode: GlobalEventHandlers;
163 | }
164 |
165 | // Export Sortable Container Component Mixin
166 | export const ContainerMixin = defineComponent({
167 | inject: {
168 | SlicksortHub: {
169 | from: 'SlicksortHub',
170 | default: null,
171 | },
172 | },
173 |
174 | provide() {
175 | return {
176 | manager: this.manager,
177 | };
178 | },
179 |
180 | props: {
181 | list: { type: Array as PropType, required: true },
182 | axis: { type: String, default: 'y' }, // 'x', 'y', 'xy'
183 | distance: { type: Number, default: 0 },
184 | pressDelay: { type: Number, default: 0 },
185 | pressThreshold: { type: Number, default: 5 },
186 | useDragHandle: { type: Boolean, default: false },
187 | useWindowAsScrollContainer: { type: Boolean, default: false },
188 | hideSortableGhost: { type: Boolean, default: true },
189 | lockToContainerEdges: { type: Boolean, default: false },
190 | lockOffset: { type: [String, Number, Array] as PropType, default: '50%' },
191 | transitionDuration: { type: Number, default: 300 },
192 | appendTo: { type: String, default: 'body' },
193 | draggedSettlingDuration: { type: Number, default: null },
194 | group: { type: String, default: '' },
195 | accept: { type: [Boolean, Array, Function] as PropType, default: null },
196 | cancelKey: { type: String, default: 'Escape' },
197 | block: { type: Array as PropType, default: () => [] },
198 | lockAxis: { type: String, default: '' },
199 | helperClass: { type: String, default: '' },
200 | contentWindow: { type: Object as PropType, default: null },
201 | shouldCancelStart: {
202 | type: Function as PropType,
203 | default: (e: PointEvent) => {
204 | // Cancel sorting if the event target is an `input`, `textarea`, `select` or `option`
205 | const disabledElements = ['input', 'textarea', 'select', 'option', 'button'];
206 | return disabledElements.indexOf((e.target as HTMLElement).tagName.toLowerCase()) !== -1;
207 | },
208 | },
209 | getHelperDimensions: {
210 | type: Function as PropType<(arg: ItemRef) => WidthHeight>,
211 | default: ({ node }: ItemRef) => ({
212 | width: node.offsetWidth,
213 | height: node.offsetHeight,
214 | }),
215 | },
216 | },
217 |
218 | emits: ['sort-start', 'sort-move', 'sort-end', 'sort-cancel', 'sort-insert', 'sort-remove', 'update:list'],
219 |
220 | data() {
221 | let useHub = false;
222 | if (this.group) {
223 | // If the group option is set, it is assumed the user intends
224 | // to drag between containers and the required plugin has been installed
225 | if (this.SlicksortHub) {
226 | useHub = true;
227 | } else if (process.env.NODE_ENV !== 'production') {
228 | throw new Error('Slicksort plugin required to use "group" prop');
229 | }
230 | }
231 |
232 | return ({
233 | sorting: false,
234 | hub: useHub ? this.SlicksortHub : null,
235 | manager: new Manager(),
236 | } as unknown) as ComponentData;
237 | },
238 |
239 | mounted() {
240 | if (this.hub) {
241 | this.id = this.hub.getId();
242 | }
243 | this.container = this.$el;
244 | this.document = this.container.ownerDocument || document;
245 | this._window = this.contentWindow || window;
246 | this.scrollContainer = this.useWindowAsScrollContainer ? { scrollLeft: 0, scrollTop: 0 } : this.container;
247 | this.events = {
248 | start: this.handleStart,
249 | move: this.handleMove,
250 | end: this.handleEnd,
251 | };
252 |
253 | for (const key in this.events) {
254 | if (hasOwnProperty(this.events, key)) {
255 | // @ts-ignore
256 | events[key].forEach((eventName) => this.container.addEventListener(eventName, this.events[key]));
257 | }
258 | }
259 |
260 | if (this.hub) {
261 | this.hub.addContainer(this as ContainerRef);
262 | }
263 | },
264 |
265 | beforeUnmount() {
266 | for (const key in this.events) {
267 | if (hasOwnProperty(this.events, key)) {
268 | // @ts-ignore
269 | events[key].forEach((eventName) => this.container.removeEventListener(eventName, this.events[key]));
270 | }
271 | }
272 |
273 | if (this.hub) {
274 | this.hub.removeContainer(this as ContainerRef);
275 | }
276 |
277 | if (this.dragendTimer) clearTimeout(this.dragendTimer);
278 | if (this.cancelTimer) clearTimeout(this.cancelTimer);
279 | if (this.pressTimer) clearTimeout(this.pressTimer);
280 | if (this.autoscrollInterval) clearInterval(this.autoscrollInterval);
281 | },
282 |
283 | methods: {
284 | handleStart(e: PointEvent) {
285 | const { distance, shouldCancelStart } = this.$props;
286 |
287 | if ((!isTouch(e) && e.button === 2) || shouldCancelStart(e)) {
288 | return false;
289 | }
290 |
291 | this._touched = true;
292 | this._pos = getPointerOffset(e);
293 | const target = e.target as HTMLElement;
294 |
295 | const node = closest(target, (el) => (el as SortableNode).sortableInfo != null) as SortableNode;
296 |
297 | if (node && node.sortableInfo && this.nodeIsChild(node) && !this.sorting) {
298 | const { useDragHandle } = this.$props;
299 | const { index } = node.sortableInfo;
300 |
301 | if (useDragHandle && !closest(target, (el) => (el as SortableNode).sortableHandle != null)) return;
302 |
303 | this.manager.active = { index };
304 |
305 | /*
306 | * Fixes a bug in Firefox where the :active state of anchor tags
307 | * prevent subsequent 'mousemove' events from being fired
308 | * (see https://github.com/clauderic/react-sortable-hoc/issues/118)
309 | */
310 | if (target.tagName.toLowerCase() === 'a') {
311 | e.preventDefault();
312 | }
313 |
314 | if (!distance) {
315 | if (this.pressDelay === 0) {
316 | this.handlePress(e);
317 | } else {
318 | this.pressTimer = timeout(() => this.handlePress(e), this.pressDelay) as Timer;
319 | }
320 | }
321 | }
322 | },
323 |
324 | nodeIsChild(node: SortableNode) {
325 | return node.sortableInfo.manager === this.manager;
326 | },
327 |
328 | handleMove(e: PointEvent) {
329 | const { distance, pressThreshold } = this.$props;
330 |
331 | if (!this.sorting && this._touched) {
332 | const offset = getPointerOffset(e);
333 | this._delta = {
334 | x: this._pos.x - offset.x,
335 | y: this._pos.y - offset.y,
336 | };
337 | const delta = Math.abs(this._delta.x) + Math.abs(this._delta.y);
338 |
339 | if (!distance && (!pressThreshold || (pressThreshold && delta >= pressThreshold))) {
340 | if (this.cancelTimer) clearTimeout(this.cancelTimer);
341 | this.cancelTimer = timeout(this.cancel, 0);
342 | } else if (distance && delta >= distance && this.manager.isActive()) {
343 | this.handlePress(e);
344 | }
345 | }
346 | },
347 |
348 | handleEnd() {
349 | if (!this._touched) return;
350 |
351 | const { distance } = this.$props;
352 |
353 | this._touched = false;
354 |
355 | if (!distance) {
356 | this.cancel();
357 | }
358 | },
359 |
360 | cancel() {
361 | if (!this.sorting) {
362 | if (this.pressTimer) clearTimeout(this.pressTimer);
363 | this.manager.active = null;
364 | if (this.hub) this.hub.cancel();
365 | }
366 | },
367 |
368 | handleSortCancel(e: KeyboardEvent | TouchEvent) {
369 | if (isTouch(e) || e.key === this.cancelKey) {
370 | this.newIndex = this.index;
371 | this.canceling = true;
372 | this.translate = { x: 0, y: 0 };
373 | this.animateNodes();
374 | this.handleSortEnd(e);
375 | }
376 | },
377 |
378 | handlePress(e: PointEvent) {
379 | e.stopPropagation();
380 | const active = this.manager.getActive();
381 |
382 | if (active) {
383 | const { getHelperDimensions, helperClass, hideSortableGhost, appendTo } = this.$props;
384 | const { node } = active;
385 | const { index } = node.sortableInfo;
386 | const margin = getElementMargin(node);
387 |
388 | const containerBoundingRect = this.container.getBoundingClientRect();
389 | const dimensions = getHelperDimensions({ index, node });
390 |
391 | this.node = node;
392 | this.margin = margin;
393 | this.width = dimensions.width;
394 | this.height = dimensions.height;
395 | this.marginOffset = {
396 | x: this.margin.left + this.margin.right,
397 | y: Math.max(this.margin.top, this.margin.bottom),
398 | };
399 | this.boundingClientRect = node.getBoundingClientRect();
400 | this.containerBoundingRect = containerBoundingRect;
401 | this.index = index;
402 | this.newIndex = index;
403 |
404 | const clonedNode = cloneNode(node);
405 |
406 | this.helper = this.document.querySelector(appendTo)!.appendChild(clonedNode);
407 |
408 | this.helper.style.position = 'fixed';
409 | this.helper.style.top = `${this.boundingClientRect.top - margin.top}px`;
410 | this.helper.style.left = `${this.boundingClientRect.left - margin.left}px`;
411 | this.helper.style.width = `${this.width}px`;
412 | this.helper.style.height = `${this.height}px`;
413 | this.helper.style.boxSizing = 'border-box';
414 | this.helper.style.pointerEvents = 'none';
415 |
416 | if (hideSortableGhost) {
417 | this.sortableGhost = node;
418 | node.style.visibility = 'hidden';
419 | node.style.opacity = '0';
420 | }
421 |
422 | if (this.hub) {
423 | this.hub.sortStart(this as ContainerRef);
424 | this.hub.helper = this.helper;
425 | this.hub.ghost = this.sortableGhost;
426 | }
427 |
428 | this.intializeOffsets(e, this.boundingClientRect);
429 | this.offsetEdge = getEdgeOffset(node, this.container);
430 |
431 | if (helperClass) {
432 | this.helper.classList.add(...helperClass.split(' '));
433 | }
434 |
435 | this.listenerNode = isTouch(e) ? node : this._window;
436 | // @ts-ignore
437 | events.move.forEach((eventName) => this.listenerNode.addEventListener(eventName, this.handleSortMove));
438 | // @ts-ignore
439 | events.end.forEach((eventName) => this.listenerNode.addEventListener(eventName, this.handleSortEnd));
440 | // @ts-ignore
441 | events.cancel.forEach((eventName) => this.listenerNode.addEventListener(eventName, this.handleSortCancel));
442 |
443 | this.sorting = true;
444 |
445 | this.$emit('sort-start', { event: e, node, index });
446 | }
447 | },
448 |
449 | handleSortMove(e: PointEvent) {
450 | e.preventDefault(); // Prevent scrolling on mobile
451 |
452 | this.updatePosition(e);
453 |
454 | if (this.hub) {
455 | const payload = this.list[this.index!];
456 | this.hub.handleSortMove(e, payload);
457 | }
458 |
459 | if (!this.hub || this.hub.isDest(this as ContainerRef)) {
460 | this.animateNodes();
461 | this.autoscroll();
462 | }
463 |
464 | this.$emit('sort-move', { event: e });
465 | },
466 |
467 | handleDropOut() {
468 | const removed = this.list[this.index!];
469 | const newValue = arrayRemove(this.list, this.index!);
470 | this.$emit('sort-remove', {
471 | oldIndex: this.index,
472 | });
473 | this.$emit('update:list', newValue);
474 | return removed;
475 | },
476 |
477 | handleDropIn(payload: unknown) {
478 | const newValue = arrayInsert(this.list, this.newIndex!, payload);
479 | this.$emit('sort-insert', {
480 | newIndex: this.newIndex,
481 | value: payload,
482 | });
483 | this.$emit('update:list', newValue);
484 | this.handleDragEnd();
485 | },
486 |
487 | handleDragOut() {
488 | if (this.autoscrollInterval) {
489 | clearInterval(this.autoscrollInterval);
490 | this.autoscrollInterval = null;
491 | }
492 | if (this.hub!.isSource(this as ContainerRef)) {
493 | // Trick to animate all nodes up
494 | this.translate = {
495 | x: 10000,
496 | y: 10000,
497 | };
498 | this.animateNodes();
499 | } else {
500 | this.manager.getRefs().forEach((ref) => {
501 | ref.node.style['transform'] = '';
502 | });
503 | this.dragendTimer = timeout(this.handleDragEnd, this.transitionDuration || 0);
504 | }
505 | },
506 |
507 | handleDragEnd() {
508 | if (this.autoscrollInterval) {
509 | clearInterval(this.autoscrollInterval);
510 | this.autoscrollInterval = null;
511 | }
512 |
513 | resetTransform(this.manager.getRefs());
514 | if (this.sortableGhost) {
515 | this.sortableGhost.remove();
516 | this.sortableGhost = null;
517 | }
518 |
519 | if (this.dragendTimer) {
520 | clearTimeout(this.dragendTimer);
521 | this.dragendTimer = null;
522 | }
523 | this.manager.active = null;
524 | this._touched = false;
525 | this.sorting = false;
526 | },
527 |
528 | intializeOffsets(e: PointEvent, clientRect: ClientRect) {
529 | const { useWindowAsScrollContainer, containerBoundingRect, _window } = this;
530 |
531 | this.marginOffset = {
532 | x: this.margin.left + this.margin.right,
533 | y: Math.max(this.margin.top, this.margin.bottom),
534 | };
535 |
536 | this._axis = {
537 | x: this.axis.indexOf('x') >= 0,
538 | y: this.axis.indexOf('y') >= 0,
539 | };
540 |
541 | this.initialOffset = getPointerOffset(e);
542 |
543 | // initialScroll;
544 | this.initialScroll = {
545 | top: this.scrollContainer.scrollTop,
546 | left: this.scrollContainer.scrollLeft,
547 | };
548 |
549 | // initialWindowScroll;
550 | this.initialWindowScroll = {
551 | top: window.pageYOffset,
552 | left: window.pageXOffset,
553 | };
554 |
555 | this.translate = { x: 0, y: 0 };
556 | this.minTranslate = {};
557 | this.maxTranslate = {};
558 |
559 | if (this._axis.x) {
560 | this.minTranslate.x =
561 | (useWindowAsScrollContainer ? 0 : containerBoundingRect.left) - clientRect.left - this.width / 2;
562 | this.maxTranslate.x =
563 | (useWindowAsScrollContainer ? _window.innerWidth : containerBoundingRect.left + containerBoundingRect.width) -
564 | clientRect.left -
565 | this.width / 2;
566 | }
567 | if (this._axis.y) {
568 | this.minTranslate.y =
569 | (useWindowAsScrollContainer ? 0 : containerBoundingRect.top) - clientRect.top - this.height / 2;
570 | this.maxTranslate.y =
571 | (useWindowAsScrollContainer
572 | ? _window.innerHeight
573 | : containerBoundingRect.top + containerBoundingRect.height) -
574 | clientRect.top -
575 | this.height / 2;
576 | }
577 | },
578 |
579 | handleDragIn(e: PointEvent, sortableGhost: SortableNode, helper: SortableNode) {
580 | if (this.hub!.isSource(this as ContainerRef)) {
581 | return;
582 | }
583 |
584 | if (this.dragendTimer) {
585 | this.handleDragEnd();
586 | clearTimeout(this.dragendTimer);
587 | this.dragendTimer = null;
588 | }
589 |
590 | const nodes = this.manager.getRefs();
591 | this.index = nodes.length;
592 | this.manager.active = { index: this.index };
593 |
594 | const containerBoundingRect = this.container.getBoundingClientRect();
595 | const helperBoundingRect = helper.getBoundingClientRect();
596 | this.containerBoundingRect = containerBoundingRect;
597 |
598 | this.sortableGhost = cloneNode(sortableGhost);
599 | this.container.appendChild(this.sortableGhost);
600 | const ghostRect = this.sortableGhost.getBoundingClientRect();
601 | this.boundingClientRect = ghostRect;
602 | this.margin = getElementMargin(this.sortableGhost);
603 | this.width = ghostRect.width;
604 | this.height = ghostRect.height;
605 |
606 | // XY coords of the inserted node, relative to the top-left corner of the container
607 | this.offsetEdge = getEdgeOffset(this.sortableGhost, this.container);
608 |
609 | this.intializeOffsets(e, ghostRect);
610 |
611 | // Move the initialOffset back to the insertion point of the
612 | // sortableGhost (end of the list), as if we had started the drag there.
613 | this.initialOffset.x += ghostRect.x - helperBoundingRect.x;
614 | this.initialOffset.y += ghostRect.y - helperBoundingRect.y;
615 |
616 | // Turn on dragging
617 | this.sorting = true;
618 | },
619 |
620 | handleSortEnd(e: PointEvent | KeyboardEvent) {
621 | // Remove the event listeners if the node is still in the DOM
622 | if (this.listenerNode) {
623 | events.move.forEach((eventName) =>
624 | // @ts-ignore
625 | this.listenerNode.removeEventListener(eventName, this.handleSortMove),
626 | );
627 | events.end.forEach((eventName) =>
628 | // @ts-ignore
629 | this.listenerNode.removeEventListener(eventName, this.handleSortEnd),
630 | );
631 | events.cancel.forEach((eventName) =>
632 | // @ts-ignore
633 | this.listenerNode.removeEventListener(eventName, this.handleSortCancel),
634 | );
635 | }
636 |
637 | const nodes = this.manager.getRefs();
638 |
639 | // Remove the helper class(es) early to give it a chance to transition back
640 | if (this.helper && this.helperClass) {
641 | this.helper.classList.remove(...this.helperClass.split(' '));
642 | }
643 |
644 | // Stop autoscroll
645 | if (this.autoscrollInterval) clearInterval(this.autoscrollInterval);
646 | this.autoscrollInterval = null;
647 |
648 | const onEnd = () => {
649 | // Remove the helper from the DOM
650 | if (this.helper) {
651 | this.helper.remove();
652 | this.helper = null;
653 | }
654 |
655 | if (this.hideSortableGhost && this.sortableGhost) {
656 | this.sortableGhost.style.visibility = '';
657 | this.sortableGhost.style.opacity = '';
658 | }
659 |
660 | resetTransform(nodes);
661 |
662 | // Update state
663 | if (this.hub && !this.hub.isDest(this as ContainerRef)) {
664 | this.canceling ? this.hub.cancel() : this.hub.handleSortEnd();
665 | } else if (this.canceling) {
666 | this.$emit('sort-cancel', { event: e });
667 | } else {
668 | this.$emit('sort-end', {
669 | event: e,
670 | oldIndex: this.index,
671 | newIndex: this.newIndex,
672 | });
673 | this.$emit('update:list', arrayMove(this.list, this.index!, this.newIndex!));
674 | }
675 |
676 | this.manager.active = null;
677 | this._touched = false;
678 | this.canceling = false;
679 | this.sorting = false;
680 | };
681 |
682 | if (this.transitionDuration || this.draggedSettlingDuration) {
683 | this.transitionHelperIntoPlace(nodes, onEnd);
684 | } else {
685 | onEnd();
686 | }
687 | },
688 |
689 | transitionHelperIntoPlace(nodes: ItemRef[], cb: () => void) {
690 | if (this.draggedSettlingDuration === 0 || nodes.length === 0 || !this.helper) {
691 | return Promise.resolve();
692 | }
693 |
694 | const indexNode = nodes[this.index!].node;
695 | let targetX = 0;
696 | let targetY = 0;
697 |
698 | const scrollDifference = {
699 | top: window.pageYOffset - this.initialWindowScroll.top,
700 | left: window.pageXOffset - this.initialWindowScroll.left,
701 | };
702 |
703 | if (this.hub && !this.hub.isDest(this as ContainerRef) && !this.canceling) {
704 | const dest = this.hub.getDest();
705 | if (!dest) return;
706 | const destIndex = dest.newIndex;
707 | const destRefs = dest.manager.getOrderedRefs();
708 | const destNode = destIndex < destRefs.length ? destRefs[destIndex].node : dest.sortableGhost!;
709 | const ancestor = commonOffsetParent(indexNode, destNode)!;
710 |
711 | const sourceOffset = getEdgeOffset(indexNode, ancestor);
712 | const targetOffset = getEdgeOffset(destNode, ancestor);
713 |
714 | targetX = targetOffset.left - sourceOffset.left - scrollDifference.left;
715 | targetY = targetOffset.top - sourceOffset.top - scrollDifference.top;
716 | } else {
717 | const newIndexNode = nodes[this.newIndex!].node;
718 | const deltaScroll = {
719 | left: this.scrollContainer.scrollLeft - this.initialScroll.left + scrollDifference.left,
720 | top: this.scrollContainer.scrollTop - this.initialScroll.top + scrollDifference.top,
721 | };
722 | targetX = -deltaScroll.left;
723 | if (this.translate && this.translate.x > 0) {
724 | // Diff against right edge when moving to the right
725 | targetX +=
726 | newIndexNode.offsetLeft + newIndexNode.offsetWidth - (indexNode.offsetLeft + indexNode.offsetWidth);
727 | } else {
728 | targetX += newIndexNode.offsetLeft - indexNode.offsetLeft;
729 | }
730 |
731 | targetY = -deltaScroll.top;
732 | if (this.translate && this.translate.y > 0) {
733 | // Diff against the bottom edge when moving down
734 | targetY +=
735 | newIndexNode.offsetTop + newIndexNode.offsetHeight - (indexNode.offsetTop + indexNode.offsetHeight);
736 | } else {
737 | targetY += newIndexNode.offsetTop - indexNode.offsetTop;
738 | }
739 | }
740 |
741 | const duration = this.draggedSettlingDuration !== null ? this.draggedSettlingDuration : this.transitionDuration;
742 |
743 | setTransform(this.helper, `translate3d(${targetX}px,${targetY}px, 0)`, `${duration}ms`);
744 |
745 | // Register an event handler to clean up styles when the transition
746 | // finishes.
747 | const cleanup = (event: TransitionEvent) => {
748 | if (!event || event.propertyName === 'transform') {
749 | clearTimeout(cleanupTimer);
750 | setTransform(this.helper);
751 | cb();
752 | }
753 | };
754 | // Force cleanup in case 'transitionend' never fires
755 | const cleanupTimer = setTimeout(cleanup, duration + 10);
756 | this.helper.addEventListener('transitionend', cleanup);
757 | },
758 |
759 | updatePosition(e: PointEvent) {
760 | const { lockAxis, lockToContainerEdges } = this.$props;
761 |
762 | const offset = getPointerOffset(e);
763 | const translate = {
764 | x: offset.x - this.initialOffset.x,
765 | y: offset.y - this.initialOffset.y,
766 | };
767 | // Adjust for window scroll
768 | translate.y -= window.pageYOffset - this.initialWindowScroll.top;
769 | translate.x -= window.pageXOffset - this.initialWindowScroll.left;
770 |
771 | this.translate = translate;
772 |
773 | if (lockToContainerEdges) {
774 | const [minLockOffset, maxLockOffset] = getLockPixelOffsets(this.lockOffset, this.height, this.width);
775 | const minOffset = {
776 | x: this.width / 2 - minLockOffset.x,
777 | y: this.height / 2 - minLockOffset.y,
778 | };
779 | const maxOffset = {
780 | x: this.width / 2 - maxLockOffset.x,
781 | y: this.height / 2 - maxLockOffset.y,
782 | };
783 |
784 | if (this.minTranslate.x && this.maxTranslate.x)
785 | translate.x = limit(this.minTranslate.x + minOffset.x, this.maxTranslate.x - maxOffset.x, translate.x);
786 | if (this.minTranslate.y && this.maxTranslate.y)
787 | translate.y = limit(this.minTranslate.y + minOffset.y, this.maxTranslate.y - maxOffset.y, translate.y);
788 | }
789 |
790 | if (lockAxis === 'x') {
791 | translate.y = 0;
792 | } else if (lockAxis === 'y') {
793 | translate.x = 0;
794 | }
795 |
796 | if (this.helper) {
797 | this.helper.style['transform'] = `translate3d(${translate.x}px,${translate.y}px, 0)`;
798 | }
799 | },
800 |
801 | animateNodes() {
802 | const { transitionDuration, hideSortableGhost } = this.$props;
803 | const nodes = this.manager.getOrderedRefs();
804 | const deltaScroll = {
805 | left: this.scrollContainer.scrollLeft - this.initialScroll.left,
806 | top: this.scrollContainer.scrollTop - this.initialScroll.top,
807 | };
808 | const sortingOffset = {
809 | left: this.offsetEdge.left + this.translate.x + deltaScroll.left,
810 | top: this.offsetEdge.top + this.translate.y + deltaScroll.top,
811 | };
812 | const scrollDifference = {
813 | top: window.pageYOffset - this.initialWindowScroll.top,
814 | left: window.pageXOffset - this.initialWindowScroll.left,
815 | };
816 | this.newIndex = null;
817 |
818 | for (let i = 0, len = nodes.length; i < len; i++) {
819 | const { node } = nodes[i];
820 | const index = node.sortableInfo.index;
821 | const width = node.offsetWidth;
822 | const height = node.offsetHeight;
823 | const offset = {
824 | width: this.width > width ? width / 2 : this.width / 2,
825 | height: this.height > height ? height / 2 : this.height / 2,
826 | };
827 |
828 | const translate = {
829 | x: 0,
830 | y: 0,
831 | };
832 | let { edgeOffset } = nodes[i];
833 |
834 | // If we haven't cached the node's offsetTop / offsetLeft value
835 | if (!edgeOffset) {
836 | nodes[i].edgeOffset = edgeOffset = getEdgeOffset(node, this.container);
837 | }
838 |
839 | // Get a reference to the next and previous node
840 | const nextNode = i < nodes.length - 1 && nodes[i + 1];
841 | const prevNode = i > 0 && nodes[i - 1];
842 |
843 | // Also cache the next node's edge offset if needed.
844 | // We need this for calculating the animation in a grid setup
845 | if (nextNode && !nextNode.edgeOffset) {
846 | nextNode.edgeOffset = getEdgeOffset(nextNode.node, this.container);
847 | }
848 |
849 | // If the node is the one we're currently animating, skip it
850 | if (index === this.index) {
851 | /*
852 | * With windowing libraries such as `react-virtualized`, the sortableGhost
853 | * node may change while scrolling down and then back up (or vice-versa),
854 | * so we need to update the reference to the new node just to be safe.
855 | */
856 | if (hideSortableGhost) {
857 | this.sortableGhost = node;
858 | node.style.visibility = 'hidden';
859 | node.style.opacity = '0';
860 | }
861 |
862 | continue;
863 | }
864 |
865 | if (transitionDuration) {
866 | node.style['transitionDuration'] = `${transitionDuration}ms`;
867 | }
868 |
869 | if (this._axis.x) {
870 | if (this._axis.y) {
871 | // Calculations for a grid setup
872 | if (
873 | index < this.index! &&
874 | ((sortingOffset.left + scrollDifference.left - offset.width <= edgeOffset.left &&
875 | sortingOffset.top + scrollDifference.top <= edgeOffset.top + offset.height) ||
876 | sortingOffset.top + scrollDifference.top + offset.height <= edgeOffset.top)
877 | ) {
878 | // If the current node is to the left on the same row, or above the node that's being dragged
879 | // then move it to the right
880 | translate.x = this.width + this.marginOffset.x;
881 | if (edgeOffset.left + translate.x > this.containerBoundingRect.width - offset.width && nextNode) {
882 | // If it moves passed the right bounds, then animate it to the first position of the next row.
883 | // We just use the offset of the next node to calculate where to move, because that node's original position
884 | // is exactly where we want to go
885 | translate.x = nextNode.edgeOffset!.left - edgeOffset.left;
886 | translate.y = nextNode.edgeOffset!.top - edgeOffset.top;
887 | }
888 | if (this.newIndex === null) {
889 | this.newIndex = index;
890 | }
891 | } else if (
892 | index > this.index! &&
893 | ((sortingOffset.left + scrollDifference.left + offset.width >= edgeOffset.left &&
894 | sortingOffset.top + scrollDifference.top + offset.height >= edgeOffset.top) ||
895 | sortingOffset.top + scrollDifference.top + offset.height >= edgeOffset.top + height)
896 | ) {
897 | // If the current node is to the right on the same row, or below the node that's being dragged
898 | // then move it to the left
899 | translate.x = -(this.width + this.marginOffset.x);
900 | if (edgeOffset.left + translate.x < this.containerBoundingRect.left + offset.width && prevNode) {
901 | // If it moves passed the left bounds, then animate it to the last position of the previous row.
902 | // We just use the offset of the previous node to calculate where to move, because that node's original position
903 | // is exactly where we want to go
904 | translate.x = prevNode.edgeOffset!.left - edgeOffset.left;
905 | translate.y = prevNode.edgeOffset!.top - edgeOffset.top;
906 | }
907 | this.newIndex = index;
908 | }
909 | } else {
910 | if (index > this.index! && sortingOffset.left + scrollDifference.left + offset.width >= edgeOffset.left) {
911 | translate.x = -(this.width + this.marginOffset.x);
912 | this.newIndex = index;
913 | } else if (
914 | index < this.index! &&
915 | sortingOffset.left + scrollDifference.left <= edgeOffset.left + offset.width
916 | ) {
917 | translate.x = this.width + this.marginOffset.x;
918 | if (this.newIndex == null) {
919 | this.newIndex = index;
920 | }
921 | }
922 | }
923 | } else if (this._axis.y) {
924 | if (index > this.index! && sortingOffset.top + scrollDifference.top + offset.height >= edgeOffset.top) {
925 | translate.y = -(this.height + this.marginOffset.y);
926 | this.newIndex = index;
927 | } else if (
928 | index < this.index! &&
929 | sortingOffset.top + scrollDifference.top <= edgeOffset.top + offset.height
930 | ) {
931 | translate.y = this.height + this.marginOffset.y;
932 | if (this.newIndex == null) {
933 | this.newIndex = index;
934 | }
935 | }
936 | }
937 | node.style['transform'] = `translate3d(${translate.x}px,${translate.y}px,0)`;
938 | }
939 |
940 | if (this.newIndex == null) {
941 | this.newIndex = this.index;
942 | }
943 | },
944 |
945 | autoscroll() {
946 | const translate = this.translate;
947 | const direction = {
948 | x: 0,
949 | y: 0,
950 | };
951 | const speed = {
952 | x: 1,
953 | y: 1,
954 | };
955 | const acceleration = {
956 | x: 10,
957 | y: 10,
958 | };
959 |
960 | if (translate.y >= this.maxTranslate.y! - this.height / 2) {
961 | direction.y = 1; // Scroll Down
962 | speed.y = acceleration.y * Math.abs((this.maxTranslate.y! - this.height / 2 - translate.y) / this.height);
963 | } else if (translate.x >= this.maxTranslate.x! - this.width / 2) {
964 | direction.x = 1; // Scroll Right
965 | speed.x = acceleration.x * Math.abs((this.maxTranslate.x! - this.width / 2 - translate.x) / this.width);
966 | } else if (translate.y <= this.minTranslate.y! + this.height / 2) {
967 | direction.y = -1; // Scroll Up
968 | speed.y = acceleration.y * Math.abs((translate.y - this.height / 2 - this.minTranslate.y!) / this.height);
969 | } else if (translate.x <= this.minTranslate.x! + this.width / 2) {
970 | direction.x = -1; // Scroll Left
971 | speed.x = acceleration.x * Math.abs((translate.x - this.width / 2 - this.minTranslate.x!) / this.width);
972 | }
973 |
974 | if (this.autoscrollInterval) {
975 | clearInterval(this.autoscrollInterval);
976 | this.autoscrollInterval = null;
977 | }
978 |
979 | if (direction.x !== 0 || direction.y !== 0) {
980 | this.autoscrollInterval = window.setInterval(() => {
981 | const offset = {
982 | left: 1 * speed.x * direction.x,
983 | top: 1 * speed.y * direction.y,
984 | };
985 |
986 | if (this.useWindowAsScrollContainer) {
987 | this._window.scrollBy(offset.left, offset.top);
988 | } else {
989 | this.scrollContainer.scrollTop += offset.top;
990 | this.scrollContainer.scrollLeft += offset.left;
991 | }
992 |
993 | this.translate.x += offset.left;
994 | this.translate.y += offset.top;
995 | this.animateNodes();
996 | }, 5);
997 | }
998 | },
999 | },
1000 | });
1001 |
--------------------------------------------------------------------------------
/src/ElementMixin.ts:
--------------------------------------------------------------------------------
1 | import { defineComponent } from 'vue';
2 | import Manager, { ItemRef } from './Manager';
3 |
4 | interface ComponentData {
5 | manager: Manager;
6 | ref: ItemRef;
7 | }
8 |
9 | // Export Sortable Element Component Mixin
10 | export const ElementMixin = defineComponent({
11 | inject: ['manager'],
12 | props: {
13 | index: {
14 | type: Number,
15 | required: true,
16 | },
17 | disabled: {
18 | type: Boolean,
19 | default: false,
20 | },
21 | },
22 |
23 | data() {
24 | return ({} as unknown) as ComponentData;
25 | },
26 |
27 | watch: {
28 | index(newIndex) {
29 | if (this.$el && this.$el.sortableInfo) {
30 | this.$el.sortableInfo.index = newIndex;
31 | }
32 | },
33 | disabled(isDisabled) {
34 | if (isDisabled) {
35 | this.removeDraggable();
36 | } else {
37 | this.setDraggable(this.index);
38 | }
39 | },
40 | },
41 |
42 | mounted() {
43 | const { disabled, index } = this.$props;
44 |
45 | if (!disabled) {
46 | this.setDraggable(index);
47 | }
48 | },
49 |
50 | beforeUnmount() {
51 | if (!this.disabled) this.removeDraggable();
52 | },
53 |
54 | methods: {
55 | setDraggable(index: number) {
56 | const node = this.$el;
57 |
58 | node.sortableInfo = {
59 | index,
60 | manager: this.manager,
61 | };
62 |
63 | this.ref = { node };
64 | this.manager.add(this.ref);
65 | },
66 |
67 | removeDraggable() {
68 | this.manager.remove(this.ref);
69 | },
70 | },
71 | });
72 |
--------------------------------------------------------------------------------
/src/HandleDirective.ts:
--------------------------------------------------------------------------------
1 | import { Directive } from 'vue';
2 |
3 | // Export Sortable Element Handle Directive
4 | export const HandleDirective: Directive = {
5 | beforeMount(el) {
6 | el.sortableHandle = true;
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/src/Manager.ts:
--------------------------------------------------------------------------------
1 | import { TopLeft } from './utils';
2 |
3 | export interface SortableNode extends HTMLElement {
4 | sortableInfo: {
5 | index: number;
6 | manager: Manager;
7 | };
8 | sortableHandle?: boolean;
9 | }
10 |
11 | export interface ItemRef {
12 | node: SortableNode;
13 | index?: number;
14 | edgeOffset?: TopLeft | null;
15 | }
16 |
17 | export default class Manager {
18 | private refs: ItemRef[] = [];
19 | public active: { index: number } | null = null;
20 |
21 | add(ref: ItemRef): void {
22 | if (!this.refs) {
23 | this.refs = [];
24 | }
25 |
26 | this.refs.push(ref);
27 | }
28 |
29 | remove(ref: ItemRef): void {
30 | const index = this.getIndex(ref);
31 |
32 | if (index !== -1) {
33 | this.refs.splice(index, 1);
34 | }
35 | }
36 |
37 | isActive(): boolean {
38 | return !!this.active;
39 | }
40 |
41 | getActive(): ItemRef | null {
42 | return this.refs.find(({ node }) => node?.sortableInfo?.index == this?.active?.index) || null;
43 | }
44 |
45 | getIndex(ref: ItemRef): number {
46 | return this.refs.indexOf(ref);
47 | }
48 |
49 | getRefs(): ItemRef[] {
50 | return this.refs;
51 | }
52 |
53 | getOrderedRefs(): ItemRef[] {
54 | return this.refs.sort((a, b) => {
55 | return a.node.sortableInfo.index - b.node.sortableInfo.index;
56 | });
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Sample.cy.jsx:
--------------------------------------------------------------------------------
1 | import { mount } from '@cypress/vue';
2 | import { SlickList, SlickItem } from './index';
3 | import { ref } from 'vue';
4 |
5 | const components = {
6 | setup() {
7 | const list = ref([1,2,3]);
8 | return () =>
9 | {list.value.map((item, i) =>
10 | Item {item}
11 | )}
12 | ;
13 | },
14 | };
15 |
16 | describe('it', () => {
17 | it('works', () => {
18 | mount(components);
19 | cy.get('cy:list cy:item').first().should('contain', 'Item').drag({ x: 80, y: 50 });
20 | cy.get('cy:list').should('have.text', 'Item 2Item 3Item 1');
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/SlicksortHub.ts:
--------------------------------------------------------------------------------
1 | import Manager from './Manager';
2 | import { getRectCenter, getDistance, getPointerOffset, isPointWithinRect, PointEvent } from './utils';
3 |
4 | let containerIDCounter = 1;
5 |
6 | export interface ContainerRef {
7 | id: string;
8 | group: string;
9 | accept: AcceptProp | null;
10 | block: string[];
11 | container: HTMLElement;
12 | newIndex: number;
13 | manager: Manager;
14 | sortableGhost: HTMLElement | null;
15 |
16 | handleDragIn(e: PointEvent, ghost: HTMLElement | null, helper: HTMLElement | null): void;
17 | handleDragOut(): void;
18 | handleDragEnd(): void;
19 | handleSortEnd(e: PointEvent): void;
20 | handleDropIn(payload: unknown): void;
21 | handleDropOut(): unknown;
22 |
23 | updatePosition(e: PointEvent): void;
24 | animateNodes(): void;
25 | autoscroll(): void;
26 | }
27 |
28 | type AcceptPropArgs = { source: ContainerRef; dest: ContainerRef; payload: unknown };
29 | export type AcceptProp = boolean | string[] | ((args: AcceptPropArgs) => boolean);
30 |
31 | /**
32 | * Always allow when dest === source
33 | * Defer to 'dest.accept()' if it is a function
34 | * Allow any group in the accept lists
35 | * Deny any group in the block list
36 | * Allow the same group by default, this can be overridden with the block prop
37 | */
38 | function canAcceptElement(dest: ContainerRef, source: ContainerRef, payload: unknown): boolean {
39 | if (source.id === dest.id) return true;
40 | if (dest.block && dest.block.includes(source.group)) return false;
41 | if (typeof dest.accept === 'function') {
42 | return dest.accept({ dest, source, payload });
43 | }
44 | if (typeof dest.accept === 'boolean') {
45 | return dest.accept;
46 | }
47 | if (dest.accept && dest.accept.includes(source.group)) return true;
48 | if (dest.group === source.group) return true;
49 | return false;
50 | }
51 |
52 | function findClosestDest(
53 | { x, y }: { x: number; y: number },
54 | refs: ContainerRef[],
55 | currentDest: ContainerRef,
56 | ): ContainerRef | null {
57 | // Quickly check if we are within the bounds of the current destination
58 | if (isPointWithinRect({ x, y }, currentDest.container.getBoundingClientRect())) {
59 | return currentDest;
60 | }
61 |
62 | let closest = null;
63 | let minDistance = Infinity;
64 | for (let i = 0; i < refs.length; i++) {
65 | const ref = refs[i];
66 | const rect = ref.container.getBoundingClientRect();
67 | const isWithin = isPointWithinRect({ x, y }, rect);
68 |
69 | if (isWithin) {
70 | // If we are within another destination, stop here
71 | return ref;
72 | }
73 |
74 | const center = getRectCenter(rect);
75 | const distance = getDistance(x, y, center.x, center.y);
76 | if (distance < minDistance) {
77 | closest = ref;
78 | minDistance = distance;
79 | }
80 | }
81 |
82 | // Try to guess the closest destination
83 | return closest;
84 | }
85 |
86 | export default class SlicksortHub {
87 | public helper: HTMLElement | null = null;
88 | public ghost: HTMLElement | null = null;
89 |
90 | private refs: ContainerRef[] = [];
91 | private source: ContainerRef | null = null;
92 | private dest: ContainerRef | null = null;
93 |
94 | getId(): string {
95 | return '' + containerIDCounter++;
96 | }
97 |
98 | isSource({ id }: ContainerRef): boolean {
99 | return this.source?.id === id;
100 | }
101 |
102 | getSource(): ContainerRef | null {
103 | return this.source;
104 | }
105 |
106 | isDest({ id }: ContainerRef): boolean {
107 | return this.dest?.id === id;
108 | }
109 |
110 | getDest(): ContainerRef | null {
111 | return this.dest;
112 | }
113 |
114 | addContainer(ref: ContainerRef): void {
115 | this.refs.push(ref);
116 | }
117 |
118 | removeContainer(ref: ContainerRef): void {
119 | this.refs = this.refs.filter((c) => c.id !== ref.id);
120 | }
121 |
122 | sortStart(ref: ContainerRef): void {
123 | this.source = ref;
124 | this.dest = ref;
125 | }
126 |
127 | handleSortMove(e: PointEvent, payload: unknown): void {
128 | const dest = this.dest;
129 | const source = this.source;
130 |
131 | if (!dest || !source) return;
132 |
133 | const refs = this.refs;
134 | const pointer = getPointerOffset(e, 'client');
135 | const newDest = findClosestDest(pointer, refs, dest) || dest;
136 |
137 | if (dest.id !== newDest.id && canAcceptElement(newDest, source, payload)) {
138 | this.dest = newDest;
139 | dest.handleDragOut();
140 | newDest.handleDragIn(e, this.ghost, this.helper);
141 | }
142 | if (dest.id !== this.source?.id) {
143 | this.dest?.updatePosition(e);
144 | this.dest?.animateNodes();
145 | this.dest?.autoscroll();
146 | }
147 | }
148 |
149 | handleSortEnd(): void {
150 | if (this.source?.id === this.dest?.id) return;
151 | const payload = this.source?.handleDropOut();
152 | this.dest?.handleDropIn(payload);
153 | this.reset();
154 | }
155 |
156 | reset(): void {
157 | this.source = null;
158 | this.dest = null;
159 | this.helper = null;
160 | this.ghost = null;
161 | }
162 |
163 | cancel(): void {
164 | this.dest?.handleDragEnd();
165 | this.reset();
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/components/DragHandle.ts:
--------------------------------------------------------------------------------
1 | import { h, defineComponent } from 'vue';
2 |
3 | export const DragHandle = defineComponent({
4 | props: {
5 | tag: {
6 | type: String,
7 | default: 'span',
8 | },
9 | },
10 | mounted() {
11 | this.$el.sortableHandle = true;
12 | },
13 | render() {
14 | return h(this.tag, this.$slots.default?.());
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/src/components/SlickItem.ts:
--------------------------------------------------------------------------------
1 | import { h, defineComponent } from 'vue';
2 | import { ElementMixin } from '../ElementMixin';
3 |
4 | export const SlickItem = defineComponent({
5 | name: 'SlickItem',
6 | mixins: [ElementMixin],
7 | props: {
8 | tag: {
9 | type: String,
10 | default: 'div',
11 | },
12 | },
13 | render() {
14 | return h(this.tag, this.$slots.default?.());
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/src/components/SlickList.ts:
--------------------------------------------------------------------------------
1 | import { h, defineComponent, PropType } from 'vue';
2 | import { ContainerMixin } from '../ContainerMixin';
3 | import { hasOwnProperty } from '../utils';
4 | import { SlickItem } from './SlickItem';
5 |
6 | export const SlickList = defineComponent({
7 | name: 'SlickList',
8 | mixins: [ContainerMixin],
9 | props: {
10 | tag: {
11 | type: String,
12 | default: 'div',
13 | },
14 | itemKey: {
15 | type: [String, Function] as PropType<((item: unknown) => string) | string>,
16 | default: 'id',
17 | },
18 | },
19 | render() {
20 | if (this.$slots.item) {
21 | return h(
22 | this.tag,
23 | this.list.map((item, index) => {
24 | let key: string;
25 | if (item == null) {
26 | return;
27 | } else if (typeof this.itemKey === 'function') {
28 | key = this.itemKey(item);
29 | } else if (
30 | typeof item === 'object' &&
31 | hasOwnProperty(item, this.itemKey) &&
32 | typeof item[this.itemKey] == 'string'
33 | ) {
34 | key = item[this.itemKey] as string;
35 | } else if (typeof item === 'string') {
36 | key = item;
37 | } else {
38 | throw new Error('Cannot find key for item, use the item-key prop and pass a function or string');
39 | }
40 | return h(
41 | SlickItem,
42 | {
43 | key,
44 | index,
45 | },
46 | {
47 | default: () => this.$slots.item?.({ item, index }),
48 | },
49 | );
50 | }),
51 | );
52 | }
53 | return h(this.tag, this.$slots.default?.());
54 | },
55 | });
56 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { ElementMixin } from './ElementMixin';
2 | export { ContainerMixin } from './ContainerMixin';
3 | export { HandleDirective } from './HandleDirective';
4 | export { SlickList } from './components/SlickList';
5 | export { SlickItem } from './components/SlickItem';
6 | export { DragHandle } from './components/DragHandle';
7 |
8 | export { arrayMove } from './utils';
9 | export { default as plugin } from './plugin';
10 |
--------------------------------------------------------------------------------
/src/plugin.ts:
--------------------------------------------------------------------------------
1 | import SlicksortHub from './SlicksortHub';
2 | import { HandleDirective } from './HandleDirective';
3 | import { Plugin } from 'vue';
4 |
5 | const plugin: Plugin = {
6 | install(app) {
7 | app.directive('drag-handle', HandleDirective);
8 | app.provide('SlicksortHub', new SlicksortHub());
9 | },
10 | };
11 |
12 | export default plugin;
13 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { ItemRef, SortableNode } from './Manager';
2 |
3 | export type XY = { x: number; y: number };
4 |
5 | export type TopLeft = { top: number; left: number };
6 | export type BottomRight = { bottom: number; right: number };
7 | export type WidthHeight = { width: number; height: number };
8 | export type Timer = ReturnType;
9 | export type PointEventName =
10 | | 'mousedown'
11 | | 'mousemove'
12 | | 'mouseup'
13 | | 'touchstart'
14 | | 'touchmove'
15 | | 'touchend'
16 | | 'touchcancel';
17 | export type PointEvent = MouseEvent | TouchEvent;
18 |
19 | export const isTouch = (e: Event): e is TouchEvent => {
20 | return (e as TouchEvent).touches != null;
21 | };
22 |
23 | // eslint-disable-next-line @typescript-eslint/ban-types
24 | export function hasOwnProperty(
25 | obj: X | null,
26 | prop: Y,
27 | ): obj is X & Record {
28 | return !!obj && Object.prototype.hasOwnProperty.call(obj, prop);
29 | }
30 |
31 | export function arrayMove(arr: T[], previousIndex: number, newIndex: number): (T | undefined)[] {
32 | const array: (T | undefined)[] = arr.slice(0);
33 | if (newIndex >= array.length) {
34 | let k = newIndex - array.length;
35 | while (k-- + 1) {
36 | array.push(undefined);
37 | }
38 | }
39 | array.splice(newIndex, 0, array.splice(previousIndex, 1)[0]);
40 | return array;
41 | }
42 |
43 | export function arrayRemove(arr: T[], previousIndex: number): T[] {
44 | const array = arr.slice(0);
45 | if (previousIndex >= array.length) return array;
46 | array.splice(previousIndex, 1);
47 | return array;
48 | }
49 |
50 | export function arrayInsert(arr: T[], newIndex: number, value: T): T[] {
51 | const array = arr.slice(0);
52 | if (newIndex === array.length) {
53 | array.push(value);
54 | } else {
55 | array.splice(newIndex, 0, value);
56 | }
57 | return array;
58 | }
59 |
60 | export const events: Record = {
61 | start: ['touchstart', 'mousedown'],
62 | move: ['touchmove', 'mousemove'],
63 | end: ['touchend', 'mouseup'],
64 | cancel: ['touchcancel', 'keyup'],
65 | };
66 |
67 | export function closest(
68 | el: SortableNode | Node | null,
69 | fn: (el: SortableNode | Node) => boolean,
70 | ): SortableNode | Node | undefined {
71 | while (el) {
72 | if (fn(el)) return el;
73 | el = el.parentNode;
74 | }
75 | }
76 |
77 | export function limit(min: number, max: number, value: number): number {
78 | if (value < min) {
79 | return min;
80 | }
81 | if (value > max) {
82 | return max;
83 | }
84 | return value;
85 | }
86 |
87 | function getCSSPixelValue(stringValue: string): number {
88 | if (stringValue.substr(-2) === 'px') {
89 | return parseFloat(stringValue);
90 | }
91 | return 0;
92 | }
93 |
94 | export function getElementMargin(element: HTMLElement): TopLeft & BottomRight {
95 | const style = window.getComputedStyle(element);
96 |
97 | return {
98 | top: getCSSPixelValue(style.marginTop),
99 | right: getCSSPixelValue(style.marginRight),
100 | bottom: getCSSPixelValue(style.marginBottom),
101 | left: getCSSPixelValue(style.marginLeft),
102 | };
103 | }
104 |
105 | export function getPointerOffset(e: PointEvent, reference: 'client' | 'page' = 'page'): XY {
106 | const x = `${reference}X` as 'clientX' | 'pageX';
107 | const y = `${reference}Y` as 'clientY' | 'pageY';
108 |
109 | return {
110 | x: isTouch(e) ? e.touches[0][x] : e[x],
111 | y: isTouch(e) ? e.touches[0][y] : e[y],
112 | };
113 | }
114 |
115 | function offsetParents(node: HTMLElement) {
116 | const nodes = [node];
117 | for (; node; node = node.offsetParent as HTMLElement) {
118 | nodes.unshift(node);
119 | }
120 | return nodes;
121 | }
122 |
123 | export function commonOffsetParent(node1: HTMLElement, node2: HTMLElement): HTMLElement | undefined {
124 | const parents1 = offsetParents(node1);
125 | const parents2 = offsetParents(node2);
126 |
127 | if (parents1[0] != parents2[0]) throw 'No common ancestor!';
128 |
129 | for (let i = 0; i < parents1.length; i++) {
130 | if (parents1[i] != parents2[i]) return parents1[i - 1];
131 | }
132 | }
133 |
134 | export function getEdgeOffset(
135 | node: HTMLElement,
136 | container: HTMLElement,
137 | offset = { top: 0, left: 0 },
138 | ): { top: number; left: number } {
139 | // Get the actual offsetTop / offsetLeft value, no matter how deep the node is nested
140 | if (node) {
141 | const nodeOffset = {
142 | top: offset.top + node.offsetTop,
143 | left: offset.left + node.offsetLeft,
144 | };
145 | if (node.offsetParent !== container.offsetParent) {
146 | return getEdgeOffset(node.offsetParent as HTMLElement, container, nodeOffset);
147 | } else {
148 | return nodeOffset;
149 | }
150 | }
151 | return { top: 0, left: 0 };
152 | }
153 |
154 | export function cloneNode(node: HTMLElement): HTMLElement {
155 | const fields = node.querySelectorAll('input, textarea, select') as NodeListOf;
156 | const clonedNode = node.cloneNode(true) as HTMLElement;
157 | const clonedFields = [...clonedNode.querySelectorAll('input, textarea, select')] as HTMLInputElement[]; // Convert NodeList to Array
158 |
159 | clonedFields.forEach((field, index) => {
160 | if (field.type !== 'file' && fields[index]) {
161 | field.value = fields[index].value;
162 | }
163 | });
164 |
165 | return clonedNode;
166 | }
167 |
168 | export function getLockPixelOffsets(lockOffset: string | number | number[], width: number, height: number): XY[] {
169 | if (typeof lockOffset == 'string') {
170 | lockOffset = +lockOffset;
171 | }
172 |
173 | if (!Array.isArray(lockOffset)) {
174 | lockOffset = [lockOffset, lockOffset];
175 | }
176 |
177 | if (lockOffset.length !== 2) {
178 | throw new Error(
179 | `lockOffset prop of SortableContainer should be a single value or an array of exactly two values. Given ${lockOffset}`,
180 | );
181 | }
182 |
183 | const [minLockOffset, maxLockOffset] = lockOffset;
184 |
185 | return [getLockPixelOffset(minLockOffset, width, height), getLockPixelOffset(maxLockOffset, width, height)];
186 | }
187 |
188 | export function getLockPixelOffset(lockOffset: number, width: number, height: number): XY {
189 | let offsetX = lockOffset;
190 | let offsetY = lockOffset;
191 | let unit = 'px';
192 |
193 | if (typeof lockOffset === 'string') {
194 | const match = /^[+-]?\d*(?:\.\d*)?(px|%)$/.exec(lockOffset);
195 |
196 | if (match === null) {
197 | throw new Error(
198 | `lockOffset value should be a number or a string of a number followed by "px" or "%". Given ${lockOffset}`,
199 | );
200 | }
201 |
202 | offsetX = offsetY = parseFloat(lockOffset);
203 | unit = match[1];
204 | }
205 |
206 | if (!isFinite(offsetX) || !isFinite(offsetY)) {
207 | throw new Error(`lockOffset value should be a finite. Given ${lockOffset}`);
208 | }
209 |
210 | if (unit === '%') {
211 | offsetX = (offsetX * width) / 100;
212 | offsetY = (offsetY * height) / 100;
213 | }
214 |
215 | return {
216 | x: offsetX,
217 | y: offsetY,
218 | };
219 | }
220 |
221 | export function getDistance(x1: number, y1: number, x2: number, y2: number): number {
222 | const x = x1 - x2;
223 | const y = y1 - y2;
224 | return Math.sqrt(x * x + y * y);
225 | }
226 |
227 | export function getRectCenter(clientRect: ClientRect): XY {
228 | return {
229 | x: clientRect.left + clientRect.width / 2,
230 | y: clientRect.top + clientRect.height / 2,
231 | };
232 | }
233 |
234 | export function resetTransform(nodes: ItemRef[] = []): void {
235 | for (let i = 0, len = nodes.length; i < len; i++) {
236 | const node = nodes[i];
237 | const el = node.node;
238 |
239 | if (!el) return;
240 |
241 | // Clear the cached offsetTop / offsetLeft value
242 | node.edgeOffset = null;
243 |
244 | // Remove the transforms / transitions
245 | setTransform(el);
246 | }
247 | }
248 |
249 | export function setTransform(el: HTMLElement | null, transform = '', duration = ''): void {
250 | if (!el) return;
251 | el.style['transform'] = transform;
252 | el.style['transitionDuration'] = duration;
253 | }
254 |
255 | function withinBounds(pos: number, top: number, bottom: number) {
256 | const upper = Math.max(top, bottom);
257 | const lower = Math.min(top, bottom);
258 | return lower <= pos && pos <= upper;
259 | }
260 |
261 | export function isPointWithinRect({ x, y }: XY, { top, left, width, height }: ClientRect): boolean {
262 | const withinX = withinBounds(x, left, left + width);
263 | const withinY = withinBounds(y, top, top + height);
264 | return withinX && withinY;
265 | }
266 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
8 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
9 | // "lib": [], /* Specify library files to be included in the compilation. */
10 | // "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | // "outDir": "./", /* Redirect output structure to the directory. */
18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | // "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | "strict": true, /* Enable all strict type-checking options. */
29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | // "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 |
43 | /* Module Resolution Options */
44 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
48 | // "typeRoots": [], /* List of folders to include type definitions from. */
49 | // "types": [], /* Type declaration files to be included in compilation. */
50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
54 |
55 | /* Source Map Options */
56 | // "sourceRoot": "./src", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
60 |
61 | /* Experimental Options */
62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
64 |
65 | /* Advanced Options */
66 | "skipLibCheck": true, /* Skip type checking of declaration files. */
67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue-jsx'
3 |
4 | export default defineConfig({
5 | plugins: [vue()],
6 | })
7 |
--------------------------------------------------------------------------------