├── .circleci └── config.yml ├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── custom-template.md │ └── feature-request.md ├── .gitignore ├── .jshintrc ├── .testcaferc.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── Sortable.js ├── Sortable.min.js ├── babel.config.js ├── bower.json ├── entry ├── entry-complete.js ├── entry-core.js └── entry-defaults.js ├── index.html ├── modular ├── sortable.complete.esm.js ├── sortable.core.esm.js └── sortable.esm.js ├── package-lock.json ├── package.json ├── plugins ├── AutoScroll │ ├── AutoScroll.js │ ├── README.md │ └── index.js ├── MultiDrag │ ├── MultiDrag.js │ ├── README.md │ └── index.js ├── OnSpill │ ├── OnSpill.js │ ├── README.md │ └── index.js ├── README.md └── Swap │ ├── README.md │ ├── Swap.js │ └── index.js ├── scripts ├── banner.js ├── build.js ├── esm-build.js ├── minify.js ├── test-compat.js ├── test.js └── umd-build.js ├── src ├── Animation.js ├── BrowserInfo.js ├── EventDispatcher.js ├── PluginManager.js ├── Sortable.js └── utils.js ├── st ├── app.js ├── iframe │ ├── frame.html │ └── index.html ├── logo.png ├── og-image.png ├── prettify │ ├── prettify.css │ ├── prettify.js │ └── run_prettify.js ├── saucelabs.svg └── theme.css └── tests ├── Sortable.compat.test.js ├── Sortable.test.js ├── dual-list.html ├── empty-list.html ├── filter.html ├── handles.html ├── nested.html ├── single-list.html └── style.css /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:10.16-browsers 6 | steps: 7 | - checkout 8 | 9 | - restore_cache: 10 | keys: 11 | - v1-dependencies-{{ checksum "package.json" }} 12 | - v1-dependencies- 13 | 14 | - run: npm install 15 | 16 | - save_cache: 17 | paths: 18 | - node_modules 19 | key: v1-dependencies-{{ checksum "package.json" }} 20 | 21 | - run: npm run build:umd 22 | 23 | - run: 24 | name: Compatibility Test 25 | command: | 26 | if [ -z "$CIRCLE_PR_NUMBER" ]; 27 | then 28 | npm run test:compat 29 | fi 30 | - run: npm run test 31 | 32 | - store_test_results: 33 | path: /tmp/test-results 34 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[bug] " 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | 30 | 31 | **Describe the bug** 32 | 33 | 34 | 35 | **To Reproduce** 36 | Steps to reproduce the behavior: 37 | 38 | 1. Go to '...' 39 | 2. Click on '....' 40 | 3. Scroll down to '....' 41 | 4. See error 42 | 43 | **Expected behavior** 44 | 45 | 46 | 47 | **Information** 48 | 49 | 50 | 51 | Versions - Look in your `package.json` for this information: 52 | sortablejs = ^x.x.x 53 | @types/sortablejs = ^x.x.x 54 | 55 | **Additional context** 56 | Add any other context about the problem here. 57 | 58 | **Reproduction** 59 | codesandbox: 60 | 61 | 74 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Not a feature request or a bug report. Usually questions, queries or concerns 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Custom** 10 | 11 | 32 | 33 | **Reproduction** 34 | codesandbox: 35 | 36 | 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[feature] " 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | 30 | 31 | **Is your feature request related to a problem? Please describe.** 32 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 33 | 34 | **Describe the solution you'd like** 35 | A clear and concise description of what you want to happen. 36 | 37 | **Describe alternatives you've considered** 38 | A clear and concise description of any alternative solutions or features you've considered. 39 | 40 | **Additional context** 41 | Add any other context or screenshots about the feature request here. 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | mock.png 3 | .*.sw* 4 | .build* 5 | jquery.fn.* 6 | .idea/ 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "strict": false, 3 | "newcap": false, 4 | "node": true, 5 | "expr": true, 6 | "supernew": true, 7 | "laxbreak": true, 8 | "esversion": 9, 9 | "white": true, 10 | "globals": { 11 | "define": true, 12 | "test": true, 13 | "expect": true, 14 | "module": true, 15 | "asyncTest": true, 16 | "start": true, 17 | "ok": true, 18 | "equal": true, 19 | "notEqual": true, 20 | "deepEqual": true, 21 | "window": true, 22 | "document": true, 23 | "performance": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.testcaferc.json: -------------------------------------------------------------------------------- 1 | { 2 | "speed": 0.4, 3 | "reporter": { 4 | "name": "xunit", 5 | "output": "/tmp/test-results/res.xml" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | ### Issue 4 | 5 | 1. Try [master](https://github.com/SortableJS/Sortable/tree/master/)-branch, perhaps the problem has been solved; 6 | 2. [Use the search](https://github.com/SortableJS/Sortable/search?type=Issues&q=problem), maybe already have an answer; 7 | 3. If not found, create example on [jsbin.com (draft)](https://jsbin.com/kamiwez/edit?html,js,output) and describe the problem. 8 | 9 | --- 10 | 11 | ### Pull Request 12 | 13 | 1. Only request to merge with the [master](https://github.com/SortableJS/Sortable/tree/master/)-branch. 14 | 2. Only modify source files, **do not commit the resulting build** 15 | 16 | ### Setup 17 | 18 | 1. Fork the repo on [github](https://github.com) 19 | 2. Clone locally 20 | 3. Run `npm i` in the local repo 21 | 22 | ### Building 23 | 24 | - For development, build the `./Sortable.js` file using the command `npm run build:umd:watch` 25 | - To build everything and minify it, run `npm run build` 26 | - Do not commit the resulting builds in any pull request – they will be generated at release 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 All contributors to Sortable 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | 4 | let presets; 5 | 6 | if (process.env.NODE_ENV === 'es') { 7 | presets = [ 8 | [ 9 | "@babel/preset-env", 10 | { 11 | "modules": false 12 | } 13 | ] 14 | ]; 15 | } else if (process.env.NODE_ENV === 'umd') { 16 | presets = [ 17 | [ 18 | "@babel/preset-env" 19 | ] 20 | ]; 21 | } 22 | 23 | return { 24 | plugins: ['@babel/plugin-transform-object-assign'], 25 | presets 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sortable", 3 | "main": [ 4 | "Sortable.js" 5 | ], 6 | "homepage": "http://SortableJS.github.io/Sortable/", 7 | "authors": [ 8 | "RubaXa ", 9 | "owenm " 10 | ], 11 | "description": "JavaScript library for reorderable drag-and-drop lists on modern browsers and touch devices. No jQuery required. Supports Meteor, AngularJS, React, Polymer, Vue, Knockout and any CSS library, e.g. Bootstrap.", 12 | "keywords": [ 13 | "sortable", 14 | "reorder", 15 | "list", 16 | "html5", 17 | "drag", 18 | "and", 19 | "drop", 20 | "dnd", 21 | "web-components" 22 | ], 23 | "license": "MIT", 24 | "ignore": [ 25 | "node_modules", 26 | "bower_components", 27 | "test", 28 | "tests" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /entry/entry-complete.js: -------------------------------------------------------------------------------- 1 | import Sortable from './entry-defaults.js'; 2 | import Swap from '../plugins/Swap'; 3 | import MultiDrag from '../plugins/MultiDrag'; 4 | 5 | Sortable.mount(new Swap()); 6 | Sortable.mount(new MultiDrag()); 7 | 8 | export default Sortable; 9 | -------------------------------------------------------------------------------- /entry/entry-core.js: -------------------------------------------------------------------------------- 1 | import Sortable from '../src/Sortable.js'; 2 | import AutoScroll from '../plugins/AutoScroll'; 3 | import OnSpill from '../plugins/OnSpill'; 4 | import Swap from '../plugins/Swap'; 5 | import MultiDrag from '../plugins/MultiDrag'; 6 | 7 | export default Sortable; 8 | 9 | export { 10 | Sortable, 11 | 12 | // Default 13 | AutoScroll, 14 | OnSpill, 15 | 16 | // Extra 17 | Swap, 18 | MultiDrag 19 | }; 20 | -------------------------------------------------------------------------------- /entry/entry-defaults.js: -------------------------------------------------------------------------------- 1 | import Sortable from '../src/Sortable.js'; 2 | import AutoScroll from '../plugins/AutoScroll'; 3 | import { RemoveOnSpill, RevertOnSpill } from '../plugins/OnSpill'; 4 | // Extra 5 | import Swap from '../plugins/Swap'; 6 | import MultiDrag from '../plugins/MultiDrag'; 7 | 8 | Sortable.mount(new AutoScroll()); 9 | Sortable.mount(RemoveOnSpill, RevertOnSpill); 10 | 11 | export default Sortable; 12 | 13 | export { 14 | Sortable, 15 | 16 | // Extra 17 | Swap, 18 | MultiDrag 19 | }; 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SortableJS 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Fork me on GitHub 20 | 21 |
22 |
23 | 24 |

SortableJS

25 |

JavaScript library for reorderable drag-and-drop lists

26 | 27 |
28 |
Features
29 |
  • Simple list
  • 30 |
  • Shared lists
  • 31 |
  • Cloning
  • 32 |
  • Disabling sorting
  • 33 |
  • Handles
  • 34 |
  • Filter
  • 35 |
  • Thresholds
  • 36 |
    Examples
    37 |
  • Grid
  • 38 |
  • Nested sortables
  • 39 |
    Plugins
    40 |
  • MultiDrag
  • 41 |
  • Swap
  • 42 |
    Comparisons
    43 |
    Framework Support
    44 |
    45 |
    46 | 47 |
    48 |

    Features

    49 |
    50 |
    51 |
    52 |

    Simple list example

    53 |
    54 |
    Item 1
    55 |
    Item 2
    56 |
    Item 3
    57 |
    Item 4
    58 |
    Item 5
    59 |
    Item 6
    60 |
    61 |
    62 |
    new Sortable(example1, {
     63 |     animation: 150,
     64 |     ghostClass: 'blue-background-class'
     65 | });
    66 |
    67 |
    68 |
    69 | 70 |
    71 |

    Shared lists

    72 |
    73 |
    Item 1
    74 |
    Item 2
    75 |
    Item 3
    76 |
    Item 4
    77 |
    Item 5
    78 |
    Item 6
    79 |
    80 | 81 |
    82 |
    Item 1
    83 |
    Item 2
    84 |
    Item 3
    85 |
    Item 4
    86 |
    Item 5
    87 |
    Item 6
    88 |
    89 |
    90 |
    new Sortable(example2Left, {
     91 |     group: 'shared', // set both lists to same group
     92 |     animation: 150
     93 | });
     94 | 
     95 | new Sortable(example2Right, {
     96 |     group: 'shared',
     97 |     animation: 150
     98 | });
    99 |
    100 |
    101 |
    102 | 103 |
    104 |

    Cloning

    105 |

    Try dragging from one list to another. The item you drag will be cloned and the clone will stay in the original list.

    106 |
    107 |
    Item 1
    108 |
    Item 2
    109 |
    Item 3
    110 |
    Item 4
    111 |
    Item 5
    112 |
    Item 6
    113 |
    114 | 115 |
    116 |
    Item 1
    117 |
    Item 2
    118 |
    Item 3
    119 |
    Item 4
    120 |
    Item 5
    121 |
    Item 6
    122 |
    123 |
    124 |
    new Sortable(example3Left, {
    125 |     group: {
    126 |         name: 'shared',
    127 |         pull: 'clone' // To clone: set pull to 'clone'
    128 |     },
    129 |     animation: 150
    130 | });
    131 | 
    132 | new Sortable(example3Right, {
    133 |     group: {
    134 |         name: 'shared',
    135 |         pull: 'clone'
    136 |     },
    137 |     animation: 150
    138 | });
    139 |
    140 |
    141 |
    142 | 143 |
    144 |

    Disabling Sorting

    145 |

    Try sorting the list on the left. It is not possible because it has it's sort option set to false. However, you can still drag from the list on the left to the list on the right.

    146 |
    147 |
    Item 1
    148 |
    Item 2
    149 |
    Item 3
    150 |
    Item 4
    151 |
    Item 5
    152 |
    Item 6
    153 |
    154 | 155 |
    156 |
    Item 1
    157 |
    Item 2
    158 |
    Item 3
    159 |
    Item 4
    160 |
    Item 5
    161 |
    Item 6
    162 |
    163 |
    164 |
    new Sortable(example4Left, {
    165 |     group: {
    166 |         name: 'shared',
    167 |         pull: 'clone',
    168 |         put: false // Do not allow items to be put into this list
    169 |     },
    170 |     animation: 150,
    171 |     sort: false // To disable sorting: set sort to false
    172 | });
    173 | 
    174 | new Sortable(example4Right, {
    175 |     group: 'shared',
    176 |     animation: 150
    177 | });
    178 |
    179 |
    180 |
    181 | 182 |
    183 |

    Handle

    184 |
    185 |
      Item 1
    186 |
      Item 2
    187 |
      Item 3
    188 |
      Item 4
    189 |
      Item 5
    190 |
      Item 6
    191 |
    192 |
    193 |
    new Sortable(example5, {
    194 |     handle: '.handle', // handle's class
    195 |     animation: 150
    196 | });
    197 |
    198 |
    199 |
    200 | 201 |
    202 |

    Filter

    203 |

    Try dragging the item with a red background. It cannot be done, because that item is filtered out using the filter option.

    204 |
    205 |
    Item 1
    206 |
    Item 2
    207 |
    Item 3
    208 |
    Filtered
    209 |
    Item 4
    210 |
    Item 5
    211 |
    212 |
    213 |
    new Sortable(example6, {
    214 |     filter: '.filtered', // 'filtered' class is not draggable
    215 |     animation: 150
    216 | });
    217 |
    218 |
    219 |
    220 | 221 |
    222 |

    Thresholds

    223 |

    Try modifying the inputs below to affect the swap thresholds. You can see the swap zones of the squares colored in dark blue, while the "dead zones" (that do not cause a swap) are colored in light blue.

    224 |
    225 |
    226 | 227 |
    228 | 229 |
    1
    230 |
    232 | 233 |
    234 | 235 |
    2
    236 |
    237 |
    238 |
    239 |
    240 |
    241 | 242 |
    243 | 244 |
    245 |
    246 |
    247 |
    Invert Swap
    248 |
    249 |
    250 | 251 |
    252 |
    253 |
    254 |
    255 | 256 | 260 |
    261 |
    262 |
    263 |
    264 |
    new Sortable(example7, {
    265 |     swapThreshold: 1,
    267 |     animation: 150
    268 | });
    269 |
    270 |
    271 | 272 | 273 |
    274 |

    Examples

    275 |
    276 |
    277 | 278 |
    279 |

    Grid Example

    280 |
    281 |
    Item 1
    Item 2
    Item 3
    Item 4
    Item 5
    Item 6
    Item 7
    Item 8
    Item 9
    Item 10
    Item 11
    Item 12
    Item 13
    Item 14
    Item 15
    Item 16
    Item 17
    Item 18
    Item 19
    Item 20
    301 |
    302 |
    303 |
    304 | 305 |
    306 |

    Nested Sortables Example

    307 |

    NOTE: When using nested Sortables with animation, it is recommended that the fallbackOnBody option is set to true.
    It is also always recommended that either the invertSwap option is set to true, or the swapThreshold option is lower than the default value of 1 (eg 0.65).

    308 |
    309 |
    Item 1.1 310 |
    311 |
    Item 2.1
    312 |
    Item 2.2 313 |
    314 |
    Item 3.1
    315 |
    Item 3.2
    316 |
    Item 3.3
    317 |
    Item 3.4
    318 |
    319 |
    320 |
    Item 2.3
    321 |
    Item 2.4
    322 |
    323 |
    324 |
    Item 1.2
    325 |
    Item 1.3
    326 |
    Item 1.4 327 |
    328 |
    Item 2.1
    329 |
    Item 2.2
    330 |
    Item 2.3
    331 |
    Item 2.4
    332 |
    333 |
    334 |
    Item 1.5
    335 |
    336 |
    337 |
    // Loop through each nested sortable element
    338 | for (var i = 0; i < nestedSortables.length; i++) {
    339 | 	new Sortable(nestedSortables[i], {
    340 | 		group: 'nested',
    341 | 		animation: 150,
    342 | 		fallbackOnBody: true,
    343 | 		swapThreshold: 0.65
    344 | 	});
    345 | }
    346 |
    347 |
    348 | 349 |
    350 |

    Plugins

    351 |
    352 |
    353 | 354 |
    355 |

    MultiDrag

    356 |

    The MultiDrag plugin allows for multiple items to be dragged at a time. You can click to "select" multiple items, and then drag them as one item.

    357 |
    358 |
    Item 1
    359 |
    Item 2
    360 |
    Item 3
    361 |
    Item 4
    362 |
    Item 5
    363 |
    Item 6
    364 |
    365 |
    366 |
    new Sortable(multiDragDemo, {
    367 | 	multiDrag: true,
    368 | 	selectedClass: 'selected',
    369 | 	fallbackTolerance: 3, // So that we can select items on mobile
    370 | 	animation: 150
    371 | });
    372 |
    373 |
    374 |
    375 | 376 |
    377 |

    Swap

    378 |

    The Swap plugin changes the behaviour of Sortable to allow for items to be swapped with eachother rather than sorted.

    379 |
    380 |
    Item 1
    381 |
    Item 2
    382 |
    Item 3
    383 |
    Item 4
    384 |
    Item 5
    385 |
    Item 6
    386 |
    387 |
    388 |
    new Sortable(swapDemo, {
    389 | 	swap: true, // Enable swap plugin
    390 | 	swapClass: 'highlight', // The class applied to the hovered swap item
    391 | 	animation: 150
    392 | });
    393 |
    394 |
    395 |
    396 | 397 | 398 | 399 |
    400 | 401 |
    402 |

    Comparisons

    403 |
    404 |
    405 | 406 | 407 |
    408 |

    jQuery-UI

    409 | 410 | 411 |

    Dragula

    412 | 413 |
    414 | 415 |
    416 | 417 |
    418 |

    Framework Support

    419 |
    420 |
    421 | 422 |
    423 | 424 |

    Vue

    425 |

    Vue.Draggable

    426 | 427 |

    React

    428 |

    react-sortablejs

    429 | 430 |

    Angular

    431 |

    ngx-sortablejs

    432 | 433 |

    jQuery

    434 |

    jquery-sortablejs

    435 | 436 |

    Knockout

    437 |

    knockout-sortablejs

    438 | 439 |

    Meteor

    440 |

    meteor-sortablejs

    441 | 442 |

    Polymer

    443 |

    polymer-sortablejs

    444 | 445 |

    Ember

    446 |

    ember-sortablejs

    447 |
    448 | 449 |
    450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sortablejs", 3 | "exportName": "Sortable", 4 | "version": "1.15.6", 5 | "devDependencies": { 6 | "@babel/core": "^7.4.4", 7 | "@babel/plugin-transform-object-assign": "^7.2.0", 8 | "@babel/preset-env": "^7.4.4", 9 | "rollup": "^1.11.3", 10 | "rollup-plugin-babel": "^4.3.2", 11 | "rollup-plugin-json": "^4.0.0", 12 | "rollup-plugin-node-resolve": "^5.0.0", 13 | "testcafe": "^1.3.1", 14 | "testcafe-browser-provider-saucelabs": "^1.7.0", 15 | "testcafe-reporter-xunit": "^2.1.0", 16 | "uglify-js": "^3.5.12" 17 | }, 18 | "description": "JavaScript library for reorderable drag-and-drop lists on modern browsers and touch devices. No jQuery required. Supports Meteor, AngularJS, React, Polymer, Vue, Knockout and any CSS library, e.g. Bootstrap.", 19 | "main": "./Sortable.min.js", 20 | "module": "modular/sortable.esm.js", 21 | "scripts": { 22 | "build:umd": "set NODE_ENV=umd&& rollup -c ./scripts/umd-build.js", 23 | "build:umd:watch": "set NODE_ENV=umd&& rollup -w -c ./scripts/umd-build.js", 24 | "build:es": "set NODE_ENV=es&& rollup -c ./scripts/esm-build.js", 25 | "build:es:watch": "set NODE_ENV=es&& rollup -w -c ./scripts/esm-build.js", 26 | "minify": "node ./scripts/minify.js", 27 | "build": "npm run build:es && npm run build:umd && npm run minify", 28 | "test:compat": "node ./scripts/test-compat.js", 29 | "test": "node ./scripts/test.js" 30 | }, 31 | "maintainers": [ 32 | "Konstantin Lebedev ", 33 | "Owen Mills " 34 | ], 35 | "repository": { 36 | "type": "git", 37 | "url": "git://github.com/SortableJS/Sortable.git" 38 | }, 39 | "files": [ 40 | "Sortable.js", 41 | "Sortable.min.js", 42 | "modular/", 43 | "src/" 44 | ], 45 | "keywords": [ 46 | "sortable", 47 | "reorder", 48 | "drag", 49 | "meteor", 50 | "angular", 51 | "ng-sortable", 52 | "react", 53 | "vue", 54 | "mixin" 55 | ], 56 | "license": "MIT" 57 | } 58 | -------------------------------------------------------------------------------- /plugins/AutoScroll/AutoScroll.js: -------------------------------------------------------------------------------- 1 | import { 2 | on, 3 | off, 4 | css, 5 | throttle, 6 | cancelThrottle, 7 | scrollBy, 8 | getParentAutoScrollElement, 9 | expando, 10 | getRect, 11 | getWindowScrollingElement 12 | } from '../../src/utils.js'; 13 | 14 | import Sortable from '../../src/Sortable.js'; 15 | 16 | import { Edge, IE11OrLess, Safari } from '../../src/BrowserInfo.js'; 17 | 18 | let autoScrolls = [], 19 | scrollEl, 20 | scrollRootEl, 21 | scrolling = false, 22 | lastAutoScrollX, 23 | lastAutoScrollY, 24 | touchEvt, 25 | pointerElemChangedInterval; 26 | 27 | function AutoScrollPlugin() { 28 | 29 | function AutoScroll() { 30 | this.defaults = { 31 | scroll: true, 32 | forceAutoScrollFallback: false, 33 | scrollSensitivity: 30, 34 | scrollSpeed: 10, 35 | bubbleScroll: true 36 | }; 37 | 38 | // Bind all private methods 39 | for (let fn in this) { 40 | if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { 41 | this[fn] = this[fn].bind(this); 42 | } 43 | } 44 | } 45 | 46 | AutoScroll.prototype = { 47 | dragStarted({ originalEvent }) { 48 | if (this.sortable.nativeDraggable) { 49 | on(document, 'dragover', this._handleAutoScroll); 50 | } else { 51 | if (this.options.supportPointer) { 52 | on(document, 'pointermove', this._handleFallbackAutoScroll); 53 | } else if (originalEvent.touches) { 54 | on(document, 'touchmove', this._handleFallbackAutoScroll); 55 | } else { 56 | on(document, 'mousemove', this._handleFallbackAutoScroll); 57 | } 58 | } 59 | }, 60 | 61 | dragOverCompleted({ originalEvent }) { 62 | // For when bubbling is canceled and using fallback (fallback 'touchmove' always reached) 63 | if (!this.options.dragOverBubble && !originalEvent.rootEl) { 64 | this._handleAutoScroll(originalEvent); 65 | } 66 | }, 67 | 68 | drop() { 69 | if (this.sortable.nativeDraggable) { 70 | off(document, 'dragover', this._handleAutoScroll); 71 | } else { 72 | off(document, 'pointermove', this._handleFallbackAutoScroll); 73 | off(document, 'touchmove', this._handleFallbackAutoScroll); 74 | off(document, 'mousemove', this._handleFallbackAutoScroll); 75 | } 76 | 77 | clearPointerElemChangedInterval(); 78 | clearAutoScrolls(); 79 | cancelThrottle(); 80 | }, 81 | 82 | nulling() { 83 | touchEvt = 84 | scrollRootEl = 85 | scrollEl = 86 | scrolling = 87 | pointerElemChangedInterval = 88 | lastAutoScrollX = 89 | lastAutoScrollY = null; 90 | 91 | autoScrolls.length = 0; 92 | }, 93 | 94 | _handleFallbackAutoScroll(evt) { 95 | this._handleAutoScroll(evt, true); 96 | }, 97 | 98 | _handleAutoScroll(evt, fallback) { 99 | const x = (evt.touches ? evt.touches[0] : evt).clientX, 100 | y = (evt.touches ? evt.touches[0] : evt).clientY, 101 | 102 | elem = document.elementFromPoint(x, y); 103 | 104 | touchEvt = evt; 105 | 106 | // IE does not seem to have native autoscroll, 107 | // Edge's autoscroll seems too conditional, 108 | // MACOS Safari does not have autoscroll, 109 | // Firefox and Chrome are good 110 | if (fallback || this.options.forceAutoScrollFallback || Edge || IE11OrLess || Safari) { 111 | autoScroll(evt, this.options, elem, fallback); 112 | 113 | // Listener for pointer element change 114 | let ogElemScroller = getParentAutoScrollElement(elem, true); 115 | if ( 116 | scrolling && 117 | ( 118 | !pointerElemChangedInterval || 119 | x !== lastAutoScrollX || 120 | y !== lastAutoScrollY 121 | ) 122 | ) { 123 | pointerElemChangedInterval && clearPointerElemChangedInterval(); 124 | // Detect for pointer elem change, emulating native DnD behaviour 125 | pointerElemChangedInterval = setInterval(() => { 126 | let newElem = getParentAutoScrollElement(document.elementFromPoint(x, y), true); 127 | if (newElem !== ogElemScroller) { 128 | ogElemScroller = newElem; 129 | clearAutoScrolls(); 130 | } 131 | autoScroll(evt, this.options, newElem, fallback); 132 | }, 10); 133 | lastAutoScrollX = x; 134 | lastAutoScrollY = y; 135 | } 136 | } else { 137 | // if DnD is enabled (and browser has good autoscrolling), first autoscroll will already scroll, so get parent autoscroll of first autoscroll 138 | if (!this.options.bubbleScroll || getParentAutoScrollElement(elem, true) === getWindowScrollingElement()) { 139 | clearAutoScrolls(); 140 | return; 141 | } 142 | autoScroll(evt, this.options, getParentAutoScrollElement(elem, false), false); 143 | } 144 | } 145 | }; 146 | 147 | return Object.assign(AutoScroll, { 148 | pluginName: 'scroll', 149 | initializeByDefault: true 150 | }); 151 | } 152 | 153 | function clearAutoScrolls() { 154 | autoScrolls.forEach(function(autoScroll) { 155 | clearInterval(autoScroll.pid); 156 | }); 157 | autoScrolls = []; 158 | } 159 | 160 | function clearPointerElemChangedInterval() { 161 | clearInterval(pointerElemChangedInterval); 162 | } 163 | 164 | 165 | const autoScroll = throttle(function(evt, options, rootEl, isFallback) { 166 | // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 167 | if (!options.scroll) return; 168 | const x = (evt.touches ? evt.touches[0] : evt).clientX, 169 | y = (evt.touches ? evt.touches[0] : evt).clientY, 170 | sens = options.scrollSensitivity, 171 | speed = options.scrollSpeed, 172 | winScroller = getWindowScrollingElement(); 173 | 174 | let scrollThisInstance = false, 175 | scrollCustomFn; 176 | 177 | // New scroll root, set scrollEl 178 | if (scrollRootEl !== rootEl) { 179 | scrollRootEl = rootEl; 180 | 181 | clearAutoScrolls(); 182 | 183 | scrollEl = options.scroll; 184 | scrollCustomFn = options.scrollFn; 185 | 186 | if (scrollEl === true) { 187 | scrollEl = getParentAutoScrollElement(rootEl, true); 188 | } 189 | } 190 | 191 | 192 | let layersOut = 0; 193 | let currentParent = scrollEl; 194 | do { 195 | let el = currentParent, 196 | rect = getRect(el), 197 | 198 | top = rect.top, 199 | bottom = rect.bottom, 200 | left = rect.left, 201 | right = rect.right, 202 | 203 | width = rect.width, 204 | height = rect.height, 205 | 206 | canScrollX, 207 | canScrollY, 208 | 209 | scrollWidth = el.scrollWidth, 210 | scrollHeight = el.scrollHeight, 211 | 212 | elCSS = css(el), 213 | 214 | scrollPosX = el.scrollLeft, 215 | scrollPosY = el.scrollTop; 216 | 217 | 218 | if (el === winScroller) { 219 | canScrollX = width < scrollWidth && (elCSS.overflowX === 'auto' || elCSS.overflowX === 'scroll' || elCSS.overflowX === 'visible'); 220 | canScrollY = height < scrollHeight && (elCSS.overflowY === 'auto' || elCSS.overflowY === 'scroll' || elCSS.overflowY === 'visible'); 221 | } else { 222 | canScrollX = width < scrollWidth && (elCSS.overflowX === 'auto' || elCSS.overflowX === 'scroll'); 223 | canScrollY = height < scrollHeight && (elCSS.overflowY === 'auto' || elCSS.overflowY === 'scroll'); 224 | } 225 | 226 | let vx = canScrollX && (Math.abs(right - x) <= sens && (scrollPosX + width) < scrollWidth) - (Math.abs(left - x) <= sens && !!scrollPosX); 227 | let vy = canScrollY && (Math.abs(bottom - y) <= sens && (scrollPosY + height) < scrollHeight) - (Math.abs(top - y) <= sens && !!scrollPosY); 228 | 229 | 230 | if (!autoScrolls[layersOut]) { 231 | for (let i = 0; i <= layersOut; i++) { 232 | if (!autoScrolls[i]) { 233 | autoScrolls[i] = {}; 234 | } 235 | } 236 | } 237 | 238 | if (autoScrolls[layersOut].vx != vx || autoScrolls[layersOut].vy != vy || autoScrolls[layersOut].el !== el) { 239 | autoScrolls[layersOut].el = el; 240 | autoScrolls[layersOut].vx = vx; 241 | autoScrolls[layersOut].vy = vy; 242 | 243 | clearInterval(autoScrolls[layersOut].pid); 244 | 245 | if (vx != 0 || vy != 0) { 246 | scrollThisInstance = true; 247 | /* jshint loopfunc:true */ 248 | autoScrolls[layersOut].pid = setInterval((function () { 249 | // emulate drag over during autoscroll (fallback), emulating native DnD behaviour 250 | if (isFallback && this.layer === 0) { 251 | Sortable.active._onTouchMove(touchEvt); // To move ghost if it is positioned absolutely 252 | } 253 | let scrollOffsetY = autoScrolls[this.layer].vy ? autoScrolls[this.layer].vy * speed : 0; 254 | let scrollOffsetX = autoScrolls[this.layer].vx ? autoScrolls[this.layer].vx * speed : 0; 255 | 256 | if (typeof(scrollCustomFn) === 'function') { 257 | if (scrollCustomFn.call(Sortable.dragged.parentNode[expando], scrollOffsetX, scrollOffsetY, evt, touchEvt, autoScrolls[this.layer].el) !== 'continue') { 258 | return; 259 | } 260 | } 261 | 262 | scrollBy(autoScrolls[this.layer].el, scrollOffsetX, scrollOffsetY); 263 | }).bind({layer: layersOut}), 24); 264 | } 265 | } 266 | layersOut++; 267 | } while (options.bubbleScroll && currentParent !== winScroller && (currentParent = getParentAutoScrollElement(currentParent, false))); 268 | scrolling = scrollThisInstance; // in case another function catches scrolling as false in between when it is not 269 | }, 30); 270 | 271 | export default AutoScrollPlugin; 272 | -------------------------------------------------------------------------------- /plugins/AutoScroll/README.md: -------------------------------------------------------------------------------- 1 | ## AutoScroll 2 | This plugin allows for the page to automatically scroll during dragging near a scrollable element's edge on mobile devices and IE9 (or whenever fallback is enabled), and also enhances most browser's native drag-and-drop autoscrolling. 3 | Demo: 4 | - `window`: https://jsbin.com/dosilir/edit?js,output 5 | - `overflow: hidden`: https://jsbin.com/xecihez/edit?html,js,output 6 | 7 | **This plugin is a default plugin, and is included in the default UMD and ESM builds of Sortable** 8 | 9 | 10 | --- 11 | 12 | 13 | ### Mounting 14 | ```js 15 | import { Sortable, AutoScroll } from 'sortablejs'; 16 | 17 | Sortable.mount(new AutoScroll()); 18 | ``` 19 | 20 | 21 | --- 22 | 23 | 24 | ### Options 25 | 26 | ```js 27 | new Sortable(el, { 28 | scroll: true, // Enable the plugin. Can be HTMLElement. 29 | forceAutoScrollFallback: false, // force autoscroll plugin to enable even when native browser autoscroll is available 30 | scrollFn: function(offsetX, offsetY, originalEvent, touchEvt, hoverTargetEl) { ... }, // if you have custom scrollbar scrollFn may be used for autoscrolling 31 | scrollSensitivity: 30, // px, how near the mouse must be to an edge to start scrolling. 32 | scrollSpeed: 10, // px, speed of the scrolling 33 | bubbleScroll: true // apply autoscroll to all parent elements, allowing for easier movement 34 | }); 35 | ``` 36 | 37 | 38 | --- 39 | 40 | 41 | #### `scroll` option 42 | Enables the plugin. Defaults to `true`. May also be set to an HTMLElement which will be where autoscrolling is rooted. 43 | 44 | **Note: Just because this plugin is enabled does not mean that it will always be used for autoscrolling. Some browsers have native drag and drop autoscroll, in which case this autoscroll plugin won't be invoked. If you wish to have this always be invoked for autoscrolling, set the option `forceAutoScrollFallback` to `true`.** 45 | 46 | Demo: 47 | - `window`: https://jsbin.com/dosilir/edit?js,output 48 | - `overflow: hidden`: https://jsbin.com/xecihez/edit?html,js,output 49 | 50 | 51 | --- 52 | 53 | 54 | #### `forceAutoScrollFallback` option 55 | Enables sortable's autoscroll even when the browser can handle it (with native drag and drop). Defaults to `false`. This will not disable the native autoscrolling. Note that setting `forceFallback: true` in the sortable options will also enable this. 56 | 57 | 58 | --- 59 | 60 | 61 | #### `scrollFn` option 62 | Useful when you have custom scrollbar with dedicated scroll function. 63 | Defines a function that will be used for autoscrolling. Sortable uses el.scrollTop/el.scrollLeft by default. Set this option if you wish to handle it differently. 64 | This function should return `'continue'` if it wishes to allow Sortable's native autoscrolling, otherwise Sortable will not scroll anything if this option is set. 65 | 66 | **Note that this option will only work if Sortable's autoscroll function is invoked.** 67 | 68 | It is invoked if any of the following are true: 69 | - The `forceFallback: true` option is set 70 | - It is a mobile device 71 | - The browser is either Safari, Internet Explorer, or Edge 72 | 73 | 74 | --- 75 | 76 | 77 | #### `scrollSensitivity` option 78 | Defines how near the mouse must be to an edge to start scrolling. 79 | 80 | **Note that this option will only work if Sortable's autoscroll function is invoked.** 81 | 82 | It is invoked if any of the following are true: 83 | - The `forceFallback: true` option is set 84 | - It is a mobile device 85 | - The browser is either Safari, Internet Explorer, or Edge 86 | 87 | 88 | --- 89 | 90 | 91 | #### `scrollSpeed` option 92 | The speed at which the window should scroll once the mouse pointer gets within the `scrollSensitivity` distance. 93 | 94 | **Note that this option will only work if Sortable's autoscroll function is invoked.** 95 | 96 | It is invoked if any of the following are true: 97 | - The `forceFallback: true` option is set 98 | - It is a mobile device 99 | - The browser is either Safari, Internet Explorer, or Edge 100 | 101 | --- 102 | 103 | 104 | #### `bubbleScroll` option 105 | If set to `true`, the normal `autoscroll` function will also be applied to all parent elements of the element the user is dragging over. 106 | 107 | Demo: https://jsbin.com/kesewor/edit?html,js,output 108 | 109 | 110 | --- 111 | -------------------------------------------------------------------------------- /plugins/AutoScroll/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './AutoScroll.js'; 2 | -------------------------------------------------------------------------------- /plugins/MultiDrag/MultiDrag.js: -------------------------------------------------------------------------------- 1 | import { 2 | toggleClass, 3 | getRect, 4 | index, 5 | closest, 6 | on, 7 | off, 8 | clone, 9 | css, 10 | setRect, 11 | unsetRect, 12 | matrix, 13 | expando 14 | } from '../../src/utils.js'; 15 | 16 | import dispatchEvent from '../../src/EventDispatcher.js'; 17 | 18 | let multiDragElements = [], 19 | multiDragClones = [], 20 | lastMultiDragSelect, // for selection with modifier key down (SHIFT) 21 | multiDragSortable, 22 | initialFolding = false, // Initial multi-drag fold when drag started 23 | folding = false, // Folding any other time 24 | dragStarted = false, 25 | dragEl, 26 | clonesFromRect, 27 | clonesHidden; 28 | 29 | function MultiDragPlugin() { 30 | function MultiDrag(sortable) { 31 | // Bind all private methods 32 | for (let fn in this) { 33 | if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { 34 | this[fn] = this[fn].bind(this); 35 | } 36 | } 37 | 38 | if (!sortable.options.avoidImplicitDeselect) { 39 | if (sortable.options.supportPointer) { 40 | on(document, 'pointerup', this._deselectMultiDrag); 41 | } else { 42 | on(document, 'mouseup', this._deselectMultiDrag); 43 | on(document, 'touchend', this._deselectMultiDrag); 44 | } 45 | } 46 | 47 | on(document, 'keydown', this._checkKeyDown); 48 | on(document, 'keyup', this._checkKeyUp); 49 | 50 | this.defaults = { 51 | selectedClass: 'sortable-selected', 52 | multiDragKey: null, 53 | avoidImplicitDeselect: false, 54 | setData(dataTransfer, dragEl) { 55 | let data = ''; 56 | if (multiDragElements.length && multiDragSortable === sortable) { 57 | multiDragElements.forEach((multiDragElement, i) => { 58 | data += (!i ? '' : ', ') + multiDragElement.textContent; 59 | }); 60 | } else { 61 | data = dragEl.textContent; 62 | } 63 | dataTransfer.setData('Text', data); 64 | } 65 | }; 66 | } 67 | 68 | MultiDrag.prototype = { 69 | multiDragKeyDown: false, 70 | isMultiDrag: false, 71 | 72 | 73 | delayStartGlobal({ dragEl: dragged }) { 74 | dragEl = dragged; 75 | }, 76 | 77 | delayEnded() { 78 | this.isMultiDrag = ~multiDragElements.indexOf(dragEl); 79 | }, 80 | 81 | setupClone({ sortable, cancel }) { 82 | if (!this.isMultiDrag) return; 83 | for (let i = 0; i < multiDragElements.length; i++) { 84 | multiDragClones.push(clone(multiDragElements[i])); 85 | 86 | multiDragClones[i].sortableIndex = multiDragElements[i].sortableIndex; 87 | 88 | multiDragClones[i].draggable = false; 89 | multiDragClones[i].style['will-change'] = ''; 90 | 91 | toggleClass(multiDragClones[i], this.options.selectedClass, false); 92 | multiDragElements[i] === dragEl && toggleClass(multiDragClones[i], this.options.chosenClass, false); 93 | } 94 | 95 | sortable._hideClone(); 96 | cancel(); 97 | }, 98 | 99 | clone({ sortable, rootEl, dispatchSortableEvent, cancel }) { 100 | if (!this.isMultiDrag) return; 101 | if (!this.options.removeCloneOnHide) { 102 | if (multiDragElements.length && multiDragSortable === sortable) { 103 | insertMultiDragClones(true, rootEl); 104 | dispatchSortableEvent('clone'); 105 | 106 | cancel(); 107 | } 108 | } 109 | }, 110 | 111 | showClone({ cloneNowShown, rootEl, cancel }) { 112 | if (!this.isMultiDrag) return; 113 | insertMultiDragClones(false, rootEl); 114 | multiDragClones.forEach(clone => { 115 | css(clone, 'display', ''); 116 | }); 117 | 118 | cloneNowShown(); 119 | clonesHidden = false; 120 | cancel(); 121 | }, 122 | 123 | hideClone({ sortable, cloneNowHidden, cancel }) { 124 | if (!this.isMultiDrag) return; 125 | multiDragClones.forEach(clone => { 126 | css(clone, 'display', 'none'); 127 | if (this.options.removeCloneOnHide && clone.parentNode) { 128 | clone.parentNode.removeChild(clone); 129 | } 130 | }); 131 | 132 | cloneNowHidden(); 133 | clonesHidden = true; 134 | cancel(); 135 | }, 136 | 137 | dragStartGlobal({ sortable }) { 138 | if (!this.isMultiDrag && multiDragSortable) { 139 | multiDragSortable.multiDrag._deselectMultiDrag(); 140 | } 141 | 142 | multiDragElements.forEach(multiDragElement => { 143 | multiDragElement.sortableIndex = index(multiDragElement); 144 | }); 145 | 146 | // Sort multi-drag elements 147 | multiDragElements = multiDragElements.sort(function(a, b) { 148 | return a.sortableIndex - b.sortableIndex; 149 | }); 150 | dragStarted = true; 151 | }, 152 | 153 | dragStarted({ sortable }) { 154 | if (!this.isMultiDrag) return; 155 | if (this.options.sort) { 156 | // Capture rects, 157 | // hide multi drag elements (by positioning them absolute), 158 | // set multi drag elements rects to dragRect, 159 | // show multi drag elements, 160 | // animate to rects, 161 | // unset rects & remove from DOM 162 | 163 | sortable.captureAnimationState(); 164 | 165 | if (this.options.animation) { 166 | multiDragElements.forEach(multiDragElement => { 167 | if (multiDragElement === dragEl) return; 168 | css(multiDragElement, 'position', 'absolute'); 169 | }); 170 | 171 | let dragRect = getRect(dragEl, false, true, true); 172 | 173 | multiDragElements.forEach(multiDragElement => { 174 | if (multiDragElement === dragEl) return; 175 | setRect(multiDragElement, dragRect); 176 | }); 177 | 178 | folding = true; 179 | initialFolding = true; 180 | } 181 | } 182 | 183 | sortable.animateAll(() => { 184 | folding = false; 185 | initialFolding = false; 186 | 187 | if (this.options.animation) { 188 | multiDragElements.forEach(multiDragElement => { 189 | unsetRect(multiDragElement); 190 | }); 191 | } 192 | 193 | // Remove all auxiliary multidrag items from el, if sorting enabled 194 | if (this.options.sort) { 195 | removeMultiDragElements(); 196 | } 197 | }); 198 | }, 199 | 200 | dragOver({ target, completed, cancel }) { 201 | if (folding && ~multiDragElements.indexOf(target)) { 202 | completed(false); 203 | cancel(); 204 | } 205 | }, 206 | 207 | revert({ fromSortable, rootEl, sortable, dragRect }) { 208 | if (multiDragElements.length > 1) { 209 | // Setup unfold animation 210 | multiDragElements.forEach(multiDragElement => { 211 | sortable.addAnimationState({ 212 | target: multiDragElement, 213 | rect: folding ? getRect(multiDragElement) : dragRect 214 | }); 215 | 216 | unsetRect(multiDragElement); 217 | 218 | multiDragElement.fromRect = dragRect; 219 | 220 | fromSortable.removeAnimationState(multiDragElement); 221 | }); 222 | folding = false; 223 | insertMultiDragElements(!this.options.removeCloneOnHide, rootEl); 224 | } 225 | }, 226 | 227 | dragOverCompleted({ sortable, isOwner, insertion, activeSortable, parentEl, putSortable }) { 228 | let options = this.options; 229 | if (insertion) { 230 | // Clones must be hidden before folding animation to capture dragRectAbsolute properly 231 | if (isOwner) { 232 | activeSortable._hideClone(); 233 | } 234 | 235 | initialFolding = false; 236 | // If leaving sort:false root, or already folding - Fold to new location 237 | if (options.animation && multiDragElements.length > 1 && (folding || !isOwner && !activeSortable.options.sort && !putSortable)) { 238 | // Fold: Set all multi drag elements's rects to dragEl's rect when multi-drag elements are invisible 239 | let dragRectAbsolute = getRect(dragEl, false, true, true); 240 | 241 | multiDragElements.forEach(multiDragElement => { 242 | if (multiDragElement === dragEl) return; 243 | setRect(multiDragElement, dragRectAbsolute); 244 | 245 | // Move element(s) to end of parentEl so that it does not interfere with multi-drag clones insertion if they are inserted 246 | // while folding, and so that we can capture them again because old sortable will no longer be fromSortable 247 | parentEl.appendChild(multiDragElement); 248 | }); 249 | 250 | folding = true; 251 | } 252 | 253 | // Clones must be shown (and check to remove multi drags) after folding when interfering multiDragElements are moved out 254 | if (!isOwner) { 255 | // Only remove if not folding (folding will remove them anyways) 256 | if (!folding) { 257 | removeMultiDragElements(); 258 | } 259 | 260 | if (multiDragElements.length > 1) { 261 | let clonesHiddenBefore = clonesHidden; 262 | activeSortable._showClone(sortable); 263 | 264 | // Unfold animation for clones if showing from hidden 265 | if (activeSortable.options.animation && !clonesHidden && clonesHiddenBefore) { 266 | multiDragClones.forEach(clone => { 267 | activeSortable.addAnimationState({ 268 | target: clone, 269 | rect: clonesFromRect 270 | }); 271 | 272 | clone.fromRect = clonesFromRect; 273 | clone.thisAnimationDuration = null; 274 | }); 275 | } 276 | } else { 277 | activeSortable._showClone(sortable); 278 | } 279 | } 280 | } 281 | }, 282 | 283 | dragOverAnimationCapture({ dragRect, isOwner, activeSortable }) { 284 | multiDragElements.forEach(multiDragElement => { 285 | multiDragElement.thisAnimationDuration = null; 286 | }); 287 | 288 | if (activeSortable.options.animation && !isOwner && activeSortable.multiDrag.isMultiDrag) { 289 | clonesFromRect = Object.assign({}, dragRect); 290 | let dragMatrix = matrix(dragEl, true); 291 | clonesFromRect.top -= dragMatrix.f; 292 | clonesFromRect.left -= dragMatrix.e; 293 | } 294 | }, 295 | 296 | dragOverAnimationComplete() { 297 | if (folding) { 298 | folding = false; 299 | removeMultiDragElements(); 300 | } 301 | }, 302 | 303 | drop({ originalEvent: evt, rootEl, parentEl, sortable, dispatchSortableEvent, oldIndex, putSortable }) { 304 | let toSortable = (putSortable || this.sortable); 305 | 306 | if (!evt) return; 307 | 308 | let options = this.options, 309 | children = parentEl.children; 310 | 311 | // Multi-drag selection 312 | if (!dragStarted) { 313 | if (options.multiDragKey && !this.multiDragKeyDown) { 314 | this._deselectMultiDrag(); 315 | } 316 | toggleClass(dragEl, options.selectedClass, !~multiDragElements.indexOf(dragEl)); 317 | 318 | if (!~multiDragElements.indexOf(dragEl)) { 319 | multiDragElements.push(dragEl); 320 | dispatchEvent({ 321 | sortable, 322 | rootEl, 323 | name: 'select', 324 | targetEl: dragEl, 325 | originalEvent: evt 326 | }); 327 | 328 | // Modifier activated, select from last to dragEl 329 | if (evt.shiftKey && lastMultiDragSelect && sortable.el.contains(lastMultiDragSelect)) { 330 | let lastIndex = index(lastMultiDragSelect), 331 | currentIndex = index(dragEl); 332 | 333 | if (~lastIndex && ~currentIndex && lastIndex !== currentIndex) { 334 | // Must include lastMultiDragSelect (select it), in case modified selection from no selection 335 | // (but previous selection existed) 336 | let n, i; 337 | if (currentIndex > lastIndex) { 338 | i = lastIndex; 339 | n = currentIndex; 340 | } else { 341 | i = currentIndex; 342 | n = lastIndex + 1; 343 | } 344 | 345 | const filter = options.filter; 346 | 347 | for (; i < n; i++) { 348 | if (~multiDragElements.indexOf(children[i])) continue; 349 | // Check if element is draggable 350 | if (!closest(children[i], options.draggable, parentEl, false)) continue; 351 | // Check if element is filtered 352 | const filtered = filter && (typeof filter === 'function' ? 353 | filter.call(sortable, evt, children[i], sortable) : 354 | filter.split(',').some((criteria) => { 355 | return closest(children[i], criteria.trim(), parentEl, false); 356 | })); 357 | if (filtered) continue; 358 | toggleClass(children[i], options.selectedClass, true); 359 | multiDragElements.push(children[i]); 360 | 361 | dispatchEvent({ 362 | sortable, 363 | rootEl, 364 | name: 'select', 365 | targetEl: children[i], 366 | originalEvent: evt 367 | }); 368 | } 369 | } 370 | } else { 371 | lastMultiDragSelect = dragEl; 372 | } 373 | 374 | multiDragSortable = toSortable; 375 | } else { 376 | multiDragElements.splice(multiDragElements.indexOf(dragEl), 1); 377 | lastMultiDragSelect = null; 378 | dispatchEvent({ 379 | sortable, 380 | rootEl, 381 | name: 'deselect', 382 | targetEl: dragEl, 383 | originalEvent: evt 384 | }); 385 | } 386 | } 387 | 388 | // Multi-drag drop 389 | if (dragStarted && this.isMultiDrag) { 390 | folding = false; 391 | // Do not "unfold" after around dragEl if reverted 392 | if ((parentEl[expando].options.sort || parentEl !== rootEl) && multiDragElements.length > 1) { 393 | let dragRect = getRect(dragEl), 394 | multiDragIndex = index(dragEl, ':not(.' + this.options.selectedClass + ')'); 395 | 396 | if (!initialFolding && options.animation) dragEl.thisAnimationDuration = null; 397 | 398 | toSortable.captureAnimationState(); 399 | 400 | if (!initialFolding) { 401 | if (options.animation) { 402 | dragEl.fromRect = dragRect; 403 | multiDragElements.forEach(multiDragElement => { 404 | multiDragElement.thisAnimationDuration = null; 405 | if (multiDragElement !== dragEl) { 406 | let rect = folding ? getRect(multiDragElement) : dragRect; 407 | multiDragElement.fromRect = rect; 408 | 409 | // Prepare unfold animation 410 | toSortable.addAnimationState({ 411 | target: multiDragElement, 412 | rect: rect 413 | }); 414 | } 415 | }); 416 | } 417 | 418 | // Multi drag elements are not necessarily removed from the DOM on drop, so to reinsert 419 | // properly they must all be removed 420 | removeMultiDragElements(); 421 | 422 | multiDragElements.forEach(multiDragElement => { 423 | if (children[multiDragIndex]) { 424 | parentEl.insertBefore(multiDragElement, children[multiDragIndex]); 425 | } else { 426 | parentEl.appendChild(multiDragElement); 427 | } 428 | multiDragIndex++; 429 | }); 430 | 431 | // If initial folding is done, the elements may have changed position because they are now 432 | // unfolding around dragEl, even though dragEl may not have his index changed, so update event 433 | // must be fired here as Sortable will not. 434 | if (oldIndex === index(dragEl)) { 435 | let update = false; 436 | multiDragElements.forEach(multiDragElement => { 437 | if (multiDragElement.sortableIndex !== index(multiDragElement)) { 438 | update = true; 439 | return; 440 | } 441 | }); 442 | 443 | if (update) { 444 | dispatchSortableEvent('update'); 445 | dispatchSortableEvent('sort'); 446 | } 447 | } 448 | } 449 | 450 | // Must be done after capturing individual rects (scroll bar) 451 | multiDragElements.forEach(multiDragElement => { 452 | unsetRect(multiDragElement); 453 | }); 454 | 455 | toSortable.animateAll(); 456 | } 457 | 458 | multiDragSortable = toSortable; 459 | } 460 | 461 | // Remove clones if necessary 462 | if (rootEl === parentEl || (putSortable && putSortable.lastPutMode !== 'clone')) { 463 | multiDragClones.forEach(clone => { 464 | clone.parentNode && clone.parentNode.removeChild(clone); 465 | }); 466 | } 467 | }, 468 | 469 | nullingGlobal() { 470 | this.isMultiDrag = 471 | dragStarted = false; 472 | multiDragClones.length = 0; 473 | }, 474 | 475 | destroyGlobal() { 476 | this._deselectMultiDrag(); 477 | off(document, 'pointerup', this._deselectMultiDrag); 478 | off(document, 'mouseup', this._deselectMultiDrag); 479 | off(document, 'touchend', this._deselectMultiDrag); 480 | 481 | off(document, 'keydown', this._checkKeyDown); 482 | off(document, 'keyup', this._checkKeyUp); 483 | }, 484 | 485 | _deselectMultiDrag(evt) { 486 | if (typeof dragStarted !== "undefined" && dragStarted) return; 487 | 488 | // Only deselect if selection is in this sortable 489 | if (multiDragSortable !== this.sortable) return; 490 | 491 | // Only deselect if target is not item in this sortable 492 | if (evt && closest(evt.target, this.options.draggable, this.sortable.el, false)) return; 493 | 494 | // Only deselect if left click 495 | if (evt && evt.button !== 0) return; 496 | 497 | while (multiDragElements.length) { 498 | let el = multiDragElements[0]; 499 | toggleClass(el, this.options.selectedClass, false); 500 | multiDragElements.shift(); 501 | dispatchEvent({ 502 | sortable: this.sortable, 503 | rootEl: this.sortable.el, 504 | name: 'deselect', 505 | targetEl: el, 506 | originalEvent: evt 507 | }); 508 | } 509 | }, 510 | 511 | _checkKeyDown(evt) { 512 | if (evt.key === this.options.multiDragKey) { 513 | this.multiDragKeyDown = true; 514 | } 515 | }, 516 | 517 | _checkKeyUp(evt) { 518 | if (evt.key === this.options.multiDragKey) { 519 | this.multiDragKeyDown = false; 520 | } 521 | } 522 | }; 523 | 524 | return Object.assign(MultiDrag, { 525 | // Static methods & properties 526 | pluginName: 'multiDrag', 527 | utils: { 528 | /** 529 | * Selects the provided multi-drag item 530 | * @param {HTMLElement} el The element to be selected 531 | */ 532 | select(el) { 533 | let sortable = el.parentNode[expando]; 534 | if (!sortable || !sortable.options.multiDrag || ~multiDragElements.indexOf(el)) return; 535 | if (multiDragSortable && multiDragSortable !== sortable) { 536 | multiDragSortable.multiDrag._deselectMultiDrag(); 537 | multiDragSortable = sortable; 538 | } 539 | toggleClass(el, sortable.options.selectedClass, true); 540 | multiDragElements.push(el); 541 | }, 542 | /** 543 | * Deselects the provided multi-drag item 544 | * @param {HTMLElement} el The element to be deselected 545 | */ 546 | deselect(el) { 547 | let sortable = el.parentNode[expando], 548 | index = multiDragElements.indexOf(el); 549 | if (!sortable || !sortable.options.multiDrag || !~index) return; 550 | toggleClass(el, sortable.options.selectedClass, false); 551 | multiDragElements.splice(index, 1); 552 | } 553 | }, 554 | eventProperties() { 555 | const oldIndicies = [], 556 | newIndicies = []; 557 | 558 | multiDragElements.forEach(multiDragElement => { 559 | oldIndicies.push({ 560 | multiDragElement, 561 | index: multiDragElement.sortableIndex 562 | }); 563 | 564 | // multiDragElements will already be sorted if folding 565 | let newIndex; 566 | if (folding && multiDragElement !== dragEl) { 567 | newIndex = -1; 568 | } else if (folding) { 569 | newIndex = index(multiDragElement, ':not(.' + this.options.selectedClass + ')'); 570 | } else { 571 | newIndex = index(multiDragElement); 572 | } 573 | newIndicies.push({ 574 | multiDragElement, 575 | index: newIndex 576 | }); 577 | }); 578 | return { 579 | items: [...multiDragElements], 580 | clones: [...multiDragClones], 581 | oldIndicies, 582 | newIndicies 583 | }; 584 | }, 585 | optionListeners: { 586 | multiDragKey(key) { 587 | key = key.toLowerCase(); 588 | if (key === 'ctrl') { 589 | key = 'Control'; 590 | } else if (key.length > 1) { 591 | key = key.charAt(0).toUpperCase() + key.substr(1); 592 | } 593 | return key; 594 | } 595 | } 596 | }); 597 | } 598 | 599 | function insertMultiDragElements(clonesInserted, rootEl) { 600 | multiDragElements.forEach((multiDragElement, i) => { 601 | let target = rootEl.children[multiDragElement.sortableIndex + (clonesInserted ? Number(i) : 0)]; 602 | if (target) { 603 | rootEl.insertBefore(multiDragElement, target); 604 | } else { 605 | rootEl.appendChild(multiDragElement); 606 | } 607 | }); 608 | } 609 | 610 | /** 611 | * Insert multi-drag clones 612 | * @param {[Boolean]} elementsInserted Whether the multi-drag elements are inserted 613 | * @param {HTMLElement} rootEl 614 | */ 615 | function insertMultiDragClones(elementsInserted, rootEl) { 616 | multiDragClones.forEach((clone, i) => { 617 | let target = rootEl.children[clone.sortableIndex + (elementsInserted ? Number(i) : 0)]; 618 | if (target) { 619 | rootEl.insertBefore(clone, target); 620 | } else { 621 | rootEl.appendChild(clone); 622 | } 623 | }); 624 | } 625 | 626 | function removeMultiDragElements() { 627 | multiDragElements.forEach(multiDragElement => { 628 | if (multiDragElement === dragEl) return; 629 | multiDragElement.parentNode && multiDragElement.parentNode.removeChild(multiDragElement); 630 | }); 631 | } 632 | 633 | export default MultiDragPlugin; 634 | -------------------------------------------------------------------------------- /plugins/MultiDrag/README.md: -------------------------------------------------------------------------------- 1 | ## MultiDrag Plugin 2 | This plugin allows users to select multiple items within a sortable at once, and drag them as one item. 3 | Once placed, the items will unfold into their original order, but all beside each other at the new position. 4 | [Read More](https://github.com/SortableJS/Sortable/wiki/Dragging-Multiple-Items-in-Sortable) 5 | 6 | Demo: https://jsbin.com/wopavom/edit?js,output 7 | 8 | 9 | --- 10 | 11 | 12 | ### Mounting 13 | ```js 14 | import { Sortable, MultiDrag } from 'sortablejs'; 15 | 16 | Sortable.mount(new MultiDrag()); 17 | ``` 18 | 19 | 20 | --- 21 | 22 | 23 | ### Options 24 | 25 | ```js 26 | new Sortable(el, { 27 | multiDrag: true, // Enable the plugin 28 | selectedClass: "sortable-selected", // Class name for selected item 29 | multiDragKey: null, // Key that must be down for items to be selected 30 | avoidImplicitDeselect: false, // true - if you don't want to deselect items on outside click 31 | 32 | // Called when an item is selected 33 | onSelect: function(/**Event*/evt) { 34 | evt.item // The selected item 35 | }, 36 | 37 | // Called when an item is deselected 38 | onDeselect: function(/**Event*/evt) { 39 | evt.item // The deselected item 40 | } 41 | }); 42 | ``` 43 | 44 | 45 | --- 46 | 47 | 48 | #### `multiDragKey` option 49 | The key that must be down for multiple items to be selected. The default is `null`, meaning no key must be down. 50 | For special keys, such as the CTRL key, simply specify the option as `'CTRL'` (casing does not matter). 51 | 52 | 53 | --- 54 | 55 | 56 | #### `selectedClass` option 57 | Class name for the selected item(s) if multiDrag is enabled. Defaults to `sortable-selected`. 58 | 59 | ```css 60 | .selected { 61 | background-color: #f9c7c8; 62 | border: solid red 1px; 63 | } 64 | ``` 65 | 66 | ```js 67 | Sortable.create(list, { 68 | multiDrag: true, 69 | selectedClass: "selected" 70 | }); 71 | ``` 72 | 73 | 74 | --- 75 | 76 | 77 | ### Event Properties 78 | - items:`HTMLElement[]` — Array of selected items, or empty 79 | - clones:`HTMLElement[]` — Array of clones, or empty 80 | - oldIndicies:`Index[]` — Array containing information on the old indicies of the selected elements. 81 | - newIndicies:`Index[]` — Array containing information on the new indicies of the selected elements. 82 | 83 | #### Index Object 84 | - element:`HTMLElement` — The element whose index is being given 85 | - index:`Number` — The index of the element 86 | 87 | #### Note on `newIndicies` 88 | For any event that is fired during sorting, the index of any selected element that is not the main dragged element is given as `-1`. 89 | This is because it has either been removed from the DOM, or because it is in a folding animation (folding to the dragged element) and will be removed after this animation is complete. 90 | 91 | 92 | --- 93 | 94 | 95 | ### Sortable.utils 96 | * select(el:`HTMLElement`) — select the given multi-drag item 97 | * deselect(el:`HTMLElement`) — deselect the given multi-drag item 98 | -------------------------------------------------------------------------------- /plugins/MultiDrag/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './MultiDrag.js'; 2 | -------------------------------------------------------------------------------- /plugins/OnSpill/OnSpill.js: -------------------------------------------------------------------------------- 1 | import { getChild } from '../../src/utils.js'; 2 | 3 | 4 | const drop = function({ 5 | originalEvent, 6 | putSortable, 7 | dragEl, 8 | activeSortable, 9 | dispatchSortableEvent, 10 | hideGhostForTarget, 11 | unhideGhostForTarget 12 | }) { 13 | if (!originalEvent) return; 14 | let toSortable = putSortable || activeSortable; 15 | hideGhostForTarget(); 16 | let touch = originalEvent.changedTouches && originalEvent.changedTouches.length ? originalEvent.changedTouches[0] : originalEvent; 17 | let target = document.elementFromPoint(touch.clientX, touch.clientY); 18 | unhideGhostForTarget(); 19 | if (toSortable && !toSortable.el.contains(target)) { 20 | dispatchSortableEvent('spill'); 21 | this.onSpill({ dragEl, putSortable }); 22 | } 23 | }; 24 | 25 | function Revert() {} 26 | 27 | Revert.prototype = { 28 | startIndex: null, 29 | dragStart({ oldDraggableIndex }) { 30 | this.startIndex = oldDraggableIndex; 31 | }, 32 | onSpill({ dragEl, putSortable }) { 33 | this.sortable.captureAnimationState(); 34 | if (putSortable) { 35 | putSortable.captureAnimationState(); 36 | } 37 | let nextSibling = getChild(this.sortable.el, this.startIndex, this.options); 38 | 39 | if (nextSibling) { 40 | this.sortable.el.insertBefore(dragEl, nextSibling); 41 | } else { 42 | this.sortable.el.appendChild(dragEl); 43 | } 44 | this.sortable.animateAll(); 45 | if (putSortable) { 46 | putSortable.animateAll(); 47 | } 48 | }, 49 | drop 50 | }; 51 | 52 | Object.assign(Revert, { 53 | pluginName: 'revertOnSpill' 54 | }); 55 | 56 | 57 | function Remove() {} 58 | 59 | Remove.prototype = { 60 | onSpill({ dragEl, putSortable }) { 61 | const parentSortable = putSortable || this.sortable; 62 | parentSortable.captureAnimationState(); 63 | dragEl.parentNode && dragEl.parentNode.removeChild(dragEl); 64 | parentSortable.animateAll(); 65 | }, 66 | drop 67 | }; 68 | 69 | Object.assign(Remove, { 70 | pluginName: 'removeOnSpill' 71 | }); 72 | 73 | 74 | export default [Remove, Revert]; 75 | 76 | export { 77 | Remove as RemoveOnSpill, 78 | Revert as RevertOnSpill 79 | }; 80 | -------------------------------------------------------------------------------- /plugins/OnSpill/README.md: -------------------------------------------------------------------------------- 1 | # OnSpill Plugins 2 | This file contains two seperate plugins, RemoveOnSpill and RevertOnSpill. They can be imported individually, or the default export (an array of both plugins) can be passed to `Sortable.mount` as well. 3 | 4 | **These plugins are default plugins, and are included in the default UMD and ESM builds of Sortable** 5 | 6 | 7 | --- 8 | 9 | 10 | ### Mounting 11 | ```js 12 | import { Sortable, OnSpill } from 'sortablejs/modular/sortable.core.esm'; 13 | 14 | Sortable.mount(OnSpill); 15 | ``` 16 | 17 | 18 | --- 19 | 20 | 21 | ## RevertOnSpill Plugin 22 | This plugin, when enabled, will cause the dragged item to be reverted to it's original position if it is spilled (ie. it is dropped outside of a valid Sortable drop target) 23 | 24 | 25 | 26 | 27 | ### Options 28 | 29 | ```js 30 | new Sortable(el, { 31 | revertOnSpill: true, // Enable plugin 32 | // Called when item is spilled 33 | onSpill: function(/**Event*/evt) { 34 | evt.item // The spilled item 35 | } 36 | }); 37 | ``` 38 | 39 | 40 | --- 41 | 42 | 43 | ## RemoveOnSpill Plugin 44 | This plugin, when enabled, will cause the dragged item to be removed from the DOM if it is spilled (ie. it is dropped outside of a valid Sortable drop target) 45 | 46 | 47 | --- 48 | 49 | 50 | ### Options 51 | 52 | ```js 53 | new Sortable(el, { 54 | removeOnSpill: true, // Enable plugin 55 | // Called when item is spilled 56 | onSpill: function(/**Event*/evt) { 57 | evt.item // The spilled item 58 | } 59 | }); 60 | ``` 61 | -------------------------------------------------------------------------------- /plugins/OnSpill/index.js: -------------------------------------------------------------------------------- 1 | export { default, RemoveOnSpill, RevertOnSpill } from './OnSpill.js'; 2 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Creating Sortable Plugins 2 | Sortable plugins are plugins that can be directly mounted to the Sortable class. They are a powerful way of modifying the default behaviour of Sortable beyond what simply using events alone allows. To mount your plugin to Sortable, it must pass a constructor function to the `Sortable.mount` function. This constructor function will be called (with the `new` keyword in front of it) whenever a Sortable instance with your plugin enabled is initialized. The constructor function will be called with the parameters `sortable` and `el`, which is the HTMLElement that the Sortable is being initialized on. This means that there will be a new instance of your plugin each time it is enabled in a Sortable. 3 | 4 | 5 | ## Constructor Parameters 6 | 7 | `sortable: Sortable` — The sortable that the plugin is being initialized on 8 | 9 | `el: HTMLElement` — The element that the sortable is being initialized on 10 | 11 | `options: Object` — The options object that the user has passed into Sortable (not merged with defaults yet) 12 | 13 | 14 | ## Static Properties 15 | The constructor function passed to `Sortable.mount` may contain several static properties and methods. The following static properties may be defined: 16 | 17 | `pluginName: String` (Required) 18 | The name of the option that the user will use in their sortable's options to enable the plugin. Should start with a lower case and be camel-cased. For example: `'multiDrag'`. This is also the property name that the plugin's instance will be under in a sortable instance (ex. `sortableInstance.multiDrag`). 19 | 20 | `utils: Object` 21 | Object containing functions that will be added to the `Sortable.utils` static object on the Sortable class. 22 | 23 | `eventOptions(eventName: String): Function` 24 | A function that is called whenever Sortable fires an event. This function should return an object to be combined with the event object that Sortable will emit. The function will be called in the context of the instance of the plugin on the Sortable that is firing the event (ie. the `this` keyword will be the plugin instance). 25 | 26 | `initializeByDefault: Boolean` 27 | Determines whether or not the plugin will always be initialized on every new Sortable instance. If this option is enabled, it does not mean that by default the plugin will be enabled on the Sortable - this must still be done in the options via the plugin's `pluginName`, or it can be enabled by default if your plugin specifies it's pluginName as a default option that is truthy. Since the plugin will already be initialized on every Sortable instance, it can also be enabled dynamically via `sortableInstance.option('pluginName', true)`. 28 | It is a good idea to have this option set to `false` if the plugin modifies the behaviour of Sortable in such a way that enabling or disabling the plugin dynamically could cause it to break. Likewise, this option should be disabled if the plugin should only be instantiated on Sortables in which that plugin is enabled. 29 | This option defaults to `true`. 30 | 31 | `optionListeners: Object` 32 | An object that may contain event listeners that are fired when a specific option is updated. 33 | These listeners are useful because the user's provided options are not necessarily unchanging once the plugin is initialized, and could be changed dynamically via the `option()` method. 34 | The listener will be fired in the context of the instance of the plugin that it is being changed in (ie. the `this` keyword will be the instance of your plugin). 35 | The name of the method should match the name of the option it listens for. The new value of the option will be passed in as an argument, and any returned value will be what the option is stored as. If no value is returned, the option will be stored as the value the user provided. 36 | 37 | Example: 38 | 39 | ```js 40 | Plugin.name = 'generateTitle'; 41 | Plugin.optionListeners = { 42 | // Listen for option 'generateTitle' 43 | generateTitle: function(title) { 44 | // Store the option in all caps 45 | return title.toUpperCase(); 46 | 47 | // OR save it to this instance of your plugin as a private field. 48 | // This way it can be accessed in events, but will not modify the user's options. 49 | this.titleAllCaps = title.toUpperCase(); 50 | } 51 | }; 52 | 53 | ``` 54 | 55 | ## Plugin Options 56 | Plugins may have custom default options or may override the defaults of other options. In order to do this, there must be a `defaults` object on the initialized plugin. This can be set in the plugin's prototype, or during the initialization of the plugin (when the `el` is available). For example: 57 | 58 | ```js 59 | function myPlugin(sortable, el, options) { 60 | this.defaults = { 61 | color: el.style.backgroundColor 62 | }; 63 | } 64 | 65 | Sortable.mount(myPlugin); 66 | ``` 67 | 68 | 69 | ## Plugin Events 70 | 71 | ### Context 72 | The events will be fired in the context of their own parent object (ie. context is not changed), however the plugin instance's Sortable instance is available under `this.sortable`. Likewise, the options are available under `this.options`. 73 | 74 | ### Event List 75 | The following table contains details on the events that a plugin may handle in the prototype of the plugin's constructor function. 76 | 77 | | Event Name | Description | Cancelable? | Cancel Behaviour | Event Type | Custom Event Object Properties | 78 | |---------------------------|------------------------------------------------------------------------------------------------------------------|-------------|----------------------------------------------------|------------|-------------------------------------------------------------------------| 79 | | filter | Fired when the element is filtered, and dragging is therefore canceled | No | - | Normal | None | 80 | | delayStart | Fired when the delay starts, even if there is no delay | Yes | Cancels sorting | Normal | None | 81 | | delayEnded | Fired when the delay ends, even if there is no delay | Yes | Cancels sorting | Normal | None | 82 | | setupClone | Fired when Sortable clones the dragged element | Yes | Cancels normal clone setup | Normal | None | 83 | | dragStart | Fired when the dragging is first started | Yes | Cancels sorting | Normal | None | 84 | | clone | Fired when the clone is inserted into the DOM (if `removeCloneOnHide: false`). Tick after dragStart. | Yes | Cancels normal clone insertion & hiding | Normal | None | 85 | | dragStarted | Fired tick after dragStart | No | - | Normal | None | 86 | | dragOver | Fired when the user drags over a sortable | Yes | Cancels normal dragover behaviour | DragOver | None | 87 | | dragOverValid | Fired when the user drags over a sortable that the dragged item can be inserted into | Yes | Cancels normal valid dragover behaviour | DragOver | None | 88 | | revert | Fired when the dragged item is reverted to it's original position when entering it's `sort:false` root | Yes | Cancels normal reverting, but is still completed() | DragOver | None | 89 | | dragOverCompleted | Fired when dragOver is completed (ie. bubbling is disabled). To check if inserted, use `inserted` even property. | No | - | DragOver | `insertion: Boolean` — Whether or not the dragged element was inserted | 90 | | dragOverAnimationCapture | Fired right before the animation state is captured in dragOver | No | - | DragOver | None | 91 | | dragOverAnimationComplete | Fired after the animation is completed after a dragOver insertion | No | - | DragOver | None | 92 | | drop | Fired on drop | Yes | Cancels normal drop behavior | Normal | None | 93 | | nulling | Fired when the plugin should preform cleanups, once all drop events have fired | No | - | Normal | None | 94 | | destroy | Fired when Sortable is destroyed | No | - | Normal | None | 95 | 96 | ### Global Events 97 | Normally, an event will only be fired in a plugin if the plugin is enabled on the Sortable from which the event is being fired. However, it sometimes may be desirable for a plugin to listen in on an event from Sortables in which it is not enabled on. This is possible with global events. For an event to be global, simply add the suffix 'Global' to the event's name (casing matters) (eg. `dragStartGlobal`). 98 | Please note that your plugin must be initialized on any Sortable from which it expects to recieve events, and that includes global events. In other words, you will want to keep the `initializeByDefault` option as it's default `true` value if your plugin needs to recieve events from Sortables it is not enabled on. 99 | Please also note that if both normal and global event handlers are set, the global event handler will always be fired before the regular one. 100 | 101 | ### Event Object 102 | An object with the following properties is passed as an argument to each plugin event when it is fired. 103 | 104 | #### Properties: 105 | 106 | `dragEl: HTMLElement` — The element being dragged 107 | 108 | `parentEl: HTMLElement` — The element that the dragged element is currently in 109 | 110 | `ghostEl: HTMLElement|undefined` — If using fallback, the element dragged under the cursor (undefined until after `dragStarted` plugin event) 111 | 112 | `rootEl: HTMLElement` — The element that the dragged element originated from 113 | 114 | `nextEl: HTMLElement` — The original next sibling of dragEl 115 | 116 | `cloneEl: HTMLElement|undefined` — The clone element (undefined until after `setupClone` plugin event) 117 | 118 | `cloneHidden: Boolean` — Whether or not the clone is hidden 119 | 120 | `dragStarted: Boolean` — Boolean indicating whether or not the dragStart event has fired 121 | 122 | `putSortable: Sortable|undefined` — The element that dragEl is dragged into from it's root, otherwise undefined 123 | 124 | `activeSortable: Sortable` — The active Sortable instance 125 | 126 | `originalEvent: Event` — The original HTML event corresponding to the Sortable event 127 | 128 | `oldIndex: Number` — The old index of dragEl 129 | 130 | `oldDraggableIndex: Number` — The old index of dragEl, only counting draggable elements 131 | 132 | `newIndex: Number` — The new index of dragEl 133 | 134 | `newDraggableIndex: Number` — The new index of dragEl, only counting draggable elements 135 | 136 | 137 | #### Methods: 138 | 139 | `cloneNowHidden()` — Function to be called if the plugin has hidden the clone 140 | 141 | `cloneNowShown()` — Function to be called if the plugin has shown the clone 142 | 143 | `hideGhostForTarget()` — Hides the fallback ghost element if CSS pointer-events are not available. Call this before using document.elementFromPoint at the mouse position. 144 | 145 | `unhideGhostForTarget()` — Unhides the ghost element. To be called after `hideGhostForTarget()`. 146 | 147 | `dispatchSortableEvent(eventName: String)` — Function that can be used to emit an event on the current sortable while sorting, with all usual event properties set (eg. indexes, rootEl, cloneEl, originalEvent, etc.). 148 | 149 | 150 | ### DragOverEvent Object 151 | This event is passed to dragover events, and extends the normal event object. 152 | 153 | #### Properties: 154 | 155 | `isOwner: Boolean` — Whether or not the dragged over sortable currently contains the dragged element 156 | 157 | `axis: String` — Direction of the dragged over sortable, `'vertical'` or `'horizontal'` 158 | 159 | `revert: Boolean` — Whether or not the dragged element is being reverted to it's original position from another position 160 | 161 | `dragRect: DOMRect` — DOMRect of the dragged element 162 | 163 | `targetRect: DOMRect` — DOMRect of the target element 164 | 165 | `canSort: Boolean` — Whether or not sorting is enabled in the dragged over sortable 166 | 167 | `fromSortable: Sortable` — The sortable that the dragged element is coming from 168 | 169 | `target: HTMLElement` — The sortable item that is being dragged over 170 | 171 | 172 | #### Methods: 173 | 174 | `onMove(target: HTMLElement, after: Boolean): Boolean|Number` — Calls the `onMove` function the user specified in the options 175 | 176 | `changed()` — Fires the `onChange` event with event properties preconfigured 177 | 178 | `completed(insertion: Boolean)` — Should be called when dragover has "completed", meaning bubbling should be stopped. If `insertion` is `true`, Sortable will treat it as if the dragged element was inserted into the sortable, and hide/show clone, set ghost class, animate, etc. 179 | -------------------------------------------------------------------------------- /plugins/Swap/README.md: -------------------------------------------------------------------------------- 1 | ## Swap Plugin 2 | This plugin modifies the behaviour of Sortable to allow for items to be swapped with eachother rather than sorted. Once dragging starts, the user can drag over other items and there will be no change in the elements. However, the item that the user drops on will be swapped with the originally dragged item. 3 | 4 | Demo: https://jsbin.com/yejehog/edit?html,js,output 5 | 6 | 7 | --- 8 | 9 | 10 | ### Mounting 11 | ```js 12 | import { Sortable, Swap } from 'sortablejs/modular/sortable.core.esm'; 13 | 14 | Sortable.mount(new Swap()); 15 | ``` 16 | 17 | 18 | --- 19 | 20 | 21 | ### Options 22 | 23 | ```js 24 | new Sortable(el, { 25 | swap: true, // Enable swap mode 26 | swapClass: "sortable-swap-highlight" // Class name for swap item (if swap mode is enabled) 27 | }); 28 | ``` 29 | 30 | 31 | --- 32 | 33 | 34 | #### `swapClass` option 35 | Class name for the item to be swapped with, if swap mode is enabled. Defaults to `sortable-swap-highlight`. 36 | 37 | ```css 38 | .highlighted { 39 | background-color: #9AB6F1; 40 | } 41 | ``` 42 | 43 | ```js 44 | Sortable.create(list, { 45 | swap: true, 46 | swapClass: "highlighted" 47 | }); 48 | ``` 49 | 50 | 51 | --- 52 | 53 | 54 | ### Event Properties 55 | - swapItem:`HTMLElement|undefined` — The element that the dragged element was swapped with 56 | -------------------------------------------------------------------------------- /plugins/Swap/Swap.js: -------------------------------------------------------------------------------- 1 | import { 2 | toggleClass, 3 | index 4 | } from '../../src/utils.js'; 5 | 6 | let lastSwapEl; 7 | 8 | 9 | function SwapPlugin() { 10 | function Swap() { 11 | this.defaults = { 12 | swapClass: 'sortable-swap-highlight' 13 | }; 14 | } 15 | 16 | Swap.prototype = { 17 | dragStart({ dragEl }) { 18 | lastSwapEl = dragEl; 19 | }, 20 | dragOverValid({ completed, target, onMove, activeSortable, changed, cancel }) { 21 | if (!activeSortable.options.swap) return; 22 | let el = this.sortable.el, 23 | options = this.options; 24 | if (target && target !== el) { 25 | let prevSwapEl = lastSwapEl; 26 | if (onMove(target) !== false) { 27 | toggleClass(target, options.swapClass, true); 28 | lastSwapEl = target; 29 | } else { 30 | lastSwapEl = null; 31 | } 32 | 33 | if (prevSwapEl && prevSwapEl !== lastSwapEl) { 34 | toggleClass(prevSwapEl, options.swapClass, false); 35 | } 36 | } 37 | changed(); 38 | 39 | completed(true); 40 | cancel(); 41 | }, 42 | drop({ activeSortable, putSortable, dragEl }) { 43 | let toSortable = (putSortable || this.sortable); 44 | let options = this.options; 45 | lastSwapEl && toggleClass(lastSwapEl, options.swapClass, false); 46 | if (lastSwapEl && (options.swap || putSortable && putSortable.options.swap)) { 47 | if (dragEl !== lastSwapEl) { 48 | toSortable.captureAnimationState(); 49 | if (toSortable !== activeSortable) activeSortable.captureAnimationState(); 50 | swapNodes(dragEl, lastSwapEl); 51 | 52 | toSortable.animateAll(); 53 | if (toSortable !== activeSortable) activeSortable.animateAll(); 54 | } 55 | } 56 | }, 57 | nulling() { 58 | lastSwapEl = null; 59 | } 60 | }; 61 | 62 | return Object.assign(Swap, { 63 | pluginName: 'swap', 64 | eventProperties() { 65 | return { 66 | swapItem: lastSwapEl 67 | }; 68 | } 69 | }); 70 | } 71 | 72 | 73 | function swapNodes(n1, n2) { 74 | let p1 = n1.parentNode, 75 | p2 = n2.parentNode, 76 | i1, i2; 77 | 78 | if (!p1 || !p2 || p1.isEqualNode(n2) || p2.isEqualNode(n1)) return; 79 | 80 | i1 = index(n1); 81 | i2 = index(n2); 82 | 83 | if (p1.isEqualNode(p2) && i1 < i2) { 84 | i2++; 85 | } 86 | p1.insertBefore(n2, p1.children[i1]); 87 | p2.insertBefore(n1, p2.children[i2]); 88 | } 89 | 90 | export default SwapPlugin; 91 | -------------------------------------------------------------------------------- /plugins/Swap/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Swap.js'; 2 | -------------------------------------------------------------------------------- /scripts/banner.js: -------------------------------------------------------------------------------- 1 | import { version } from '../package.json'; 2 | 3 | export default `/**! 4 | * Sortable ${ version } 5 | * @author RubaXa 6 | * @author owenm 7 | * @license MIT 8 | */`; 9 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import json from 'rollup-plugin-json'; 3 | import resolve from 'rollup-plugin-node-resolve'; 4 | import banner from './banner.js'; 5 | 6 | 7 | export default { 8 | output: { 9 | banner, 10 | name: 'Sortable' 11 | }, 12 | plugins: [ 13 | json(), 14 | babel(), 15 | resolve() 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /scripts/esm-build.js: -------------------------------------------------------------------------------- 1 | import build from './build.js'; 2 | 3 | export default ([ 4 | { 5 | input: 'entry/entry-core.js', 6 | output: Object.assign({}, build.output, { 7 | file: 'modular/sortable.core.esm.js', 8 | format: 'esm' 9 | }) 10 | }, 11 | { 12 | input: 'entry/entry-defaults.js', 13 | output: Object.assign({}, build.output, { 14 | file: 'modular/sortable.esm.js', 15 | format: 'esm' 16 | }) 17 | }, 18 | { 19 | input: 'entry/entry-complete.js', 20 | output: Object.assign({}, build.output, { 21 | file: 'modular/sortable.complete.esm.js', 22 | format: 'esm' 23 | }) 24 | } 25 | ]).map(config => { 26 | let buildCopy = { ...build }; 27 | return Object.assign(buildCopy, config); 28 | }); 29 | -------------------------------------------------------------------------------- /scripts/minify.js: -------------------------------------------------------------------------------- 1 | const UglifyJS = require('uglify-js'), 2 | fs = require('fs'), 3 | package = require('../package.json'); 4 | 5 | const banner = `/*! Sortable ${ package.version } - ${ package.license } | ${ package.repository.url } */\n`; 6 | 7 | fs.writeFileSync( 8 | `./Sortable.min.js`, 9 | banner + UglifyJS.minify(fs.readFileSync(`./Sortable.js`, 'utf8')).code, 10 | 'utf8' 11 | ); 12 | -------------------------------------------------------------------------------- /scripts/test-compat.js: -------------------------------------------------------------------------------- 1 | const createTestCafe = require('testcafe'); 2 | // Testcafe cannot test on IE < 11 3 | // Testcafe testing on Chrome Android is currently broken (https://github.com/DevExpress/testcafe/issues/3948) 4 | const browsers = [ 5 | 'saucelabs:Internet Explorer@11.285:Windows 10', 6 | 'saucelabs:MicrosoftEdge@16.16299:Windows 10', 7 | 'saucelabs:iPhone XS Simulator@12.2', 8 | 'saucelabs:Safari@12.0:macOS 10.14', 9 | 'chrome:headless', 10 | 'firefox:headless' 11 | ]; 12 | 13 | let testcafe; 14 | let runner; 15 | let failedCount; 16 | 17 | createTestCafe(null, 8000, 8001).then((tc) => { 18 | testcafe = tc; 19 | runner = tc.createRunner(); 20 | return runner 21 | .src('./tests/Sortable.compat.test.js') 22 | .browsers(browsers) 23 | .run(); 24 | }).then((actualFailedCount) => { 25 | // https://testcafe-discuss.devexpress.com/t/why-circleci-marked-build-as-green-even-if-this-build-contain-failed-test/726/2 26 | failedCount = actualFailedCount; 27 | return testcafe.close(); 28 | }).then(() => process.exit(failedCount)); 29 | 30 | 31 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | const createTestCafe = require('testcafe'); 2 | 3 | let testcafe; 4 | let runner; 5 | let failedCount; 6 | 7 | 8 | createTestCafe().then((tc) => { 9 | testcafe = tc; 10 | runner = tc.createRunner(); 11 | return runner 12 | .src('./tests/Sortable.test.js') 13 | .browsers('chrome:headless') 14 | .concurrency(3) 15 | .run(); 16 | }).then((actualFailedCount) => { 17 | failedCount = actualFailedCount; 18 | console.log('FAILED COUNT', actualFailedCount) 19 | return testcafe.close(); 20 | }).then(() => process.exit(failedCount)); 21 | 22 | -------------------------------------------------------------------------------- /scripts/umd-build.js: -------------------------------------------------------------------------------- 1 | import build from './build.js'; 2 | 3 | 4 | export default ([ 5 | { 6 | input: 'entry/entry-complete.js', 7 | output: Object.assign({}, build.output, { 8 | file: './Sortable.js', 9 | format: 'umd' 10 | }) 11 | } 12 | ]).map(config => { 13 | let buildCopy = { ...build }; 14 | return Object.assign(buildCopy, config); 15 | }); 16 | -------------------------------------------------------------------------------- /src/Animation.js: -------------------------------------------------------------------------------- 1 | import { getRect, css, matrix, isRectEqual, indexOfObject } from './utils.js'; 2 | import Sortable from './Sortable.js'; 3 | 4 | export default function AnimationStateManager() { 5 | let animationStates = [], 6 | animationCallbackId; 7 | 8 | return { 9 | captureAnimationState() { 10 | animationStates = []; 11 | if (!this.options.animation) return; 12 | let children = [].slice.call(this.el.children); 13 | 14 | children.forEach(child => { 15 | if (css(child, 'display') === 'none' || child === Sortable.ghost) return; 16 | animationStates.push({ 17 | target: child, 18 | rect: getRect(child) 19 | }); 20 | let fromRect = { ...animationStates[animationStates.length - 1].rect }; 21 | 22 | // If animating: compensate for current animation 23 | if (child.thisAnimationDuration) { 24 | let childMatrix = matrix(child, true); 25 | if (childMatrix) { 26 | fromRect.top -= childMatrix.f; 27 | fromRect.left -= childMatrix.e; 28 | } 29 | } 30 | 31 | child.fromRect = fromRect; 32 | }); 33 | }, 34 | 35 | addAnimationState(state) { 36 | animationStates.push(state); 37 | }, 38 | 39 | removeAnimationState(target) { 40 | animationStates.splice(indexOfObject(animationStates, { target }), 1); 41 | }, 42 | 43 | animateAll(callback) { 44 | if (!this.options.animation) { 45 | clearTimeout(animationCallbackId); 46 | if (typeof(callback) === 'function') callback(); 47 | return; 48 | } 49 | 50 | let animating = false, 51 | animationTime = 0; 52 | 53 | animationStates.forEach((state) => { 54 | let time = 0, 55 | animatingThis = false, 56 | target = state.target, 57 | fromRect = target.fromRect, 58 | toRect = getRect(target), 59 | prevFromRect = target.prevFromRect, 60 | prevToRect = target.prevToRect, 61 | animatingRect = state.rect, 62 | targetMatrix = matrix(target, true); 63 | 64 | 65 | if (targetMatrix) { 66 | // Compensate for current animation 67 | toRect.top -= targetMatrix.f; 68 | toRect.left -= targetMatrix.e; 69 | } 70 | 71 | target.toRect = toRect; 72 | 73 | if (target.thisAnimationDuration) { 74 | // Could also check if animatingRect is between fromRect and toRect 75 | if ( 76 | isRectEqual(prevFromRect, toRect) && 77 | !isRectEqual(fromRect, toRect) && 78 | // Make sure animatingRect is on line between toRect & fromRect 79 | (animatingRect.top - toRect.top) / 80 | (animatingRect.left - toRect.left) === 81 | (fromRect.top - toRect.top) / 82 | (fromRect.left - toRect.left) 83 | ) { 84 | // If returning to same place as started from animation and on same axis 85 | time = calculateRealTime(animatingRect, prevFromRect, prevToRect, this.options); 86 | } 87 | } 88 | 89 | // if fromRect != toRect: animate 90 | if (!isRectEqual(toRect, fromRect)) { 91 | target.prevFromRect = fromRect; 92 | target.prevToRect = toRect; 93 | 94 | if (!time) { 95 | time = this.options.animation; 96 | } 97 | this.animate( 98 | target, 99 | animatingRect, 100 | toRect, 101 | time 102 | ); 103 | } 104 | 105 | if (time) { 106 | animating = true; 107 | animationTime = Math.max(animationTime, time); 108 | clearTimeout(target.animationResetTimer); 109 | target.animationResetTimer = setTimeout(function() { 110 | target.animationTime = 0; 111 | target.prevFromRect = null; 112 | target.fromRect = null; 113 | target.prevToRect = null; 114 | target.thisAnimationDuration = null; 115 | }, time); 116 | target.thisAnimationDuration = time; 117 | } 118 | }); 119 | 120 | 121 | clearTimeout(animationCallbackId); 122 | if (!animating) { 123 | if (typeof(callback) === 'function') callback(); 124 | } else { 125 | animationCallbackId = setTimeout(function() { 126 | if (typeof(callback) === 'function') callback(); 127 | }, animationTime); 128 | } 129 | animationStates = []; 130 | }, 131 | 132 | animate(target, currentRect, toRect, duration) { 133 | if (duration) { 134 | css(target, 'transition', ''); 135 | css(target, 'transform', ''); 136 | let elMatrix = matrix(this.el), 137 | scaleX = elMatrix && elMatrix.a, 138 | scaleY = elMatrix && elMatrix.d, 139 | translateX = (currentRect.left - toRect.left) / (scaleX || 1), 140 | translateY = (currentRect.top - toRect.top) / (scaleY || 1); 141 | 142 | target.animatingX = !!translateX; 143 | target.animatingY = !!translateY; 144 | 145 | css(target, 'transform', 'translate3d(' + translateX + 'px,' + translateY + 'px,0)'); 146 | 147 | this.forRepaintDummy = repaint(target); // repaint 148 | 149 | css(target, 'transition', 'transform ' + duration + 'ms' + (this.options.easing ? ' ' + this.options.easing : '')); 150 | css(target, 'transform', 'translate3d(0,0,0)'); 151 | (typeof target.animated === 'number') && clearTimeout(target.animated); 152 | target.animated = setTimeout(function () { 153 | css(target, 'transition', ''); 154 | css(target, 'transform', ''); 155 | target.animated = false; 156 | 157 | target.animatingX = false; 158 | target.animatingY = false; 159 | }, duration); 160 | } 161 | } 162 | }; 163 | } 164 | 165 | function repaint(target) { 166 | return target.offsetWidth; 167 | } 168 | 169 | 170 | function calculateRealTime(animatingRect, fromRect, toRect, options) { 171 | return ( 172 | Math.sqrt(Math.pow(fromRect.top - animatingRect.top, 2) + Math.pow(fromRect.left - animatingRect.left, 2)) / 173 | Math.sqrt(Math.pow(fromRect.top - toRect.top, 2) + Math.pow(fromRect.left - toRect.left, 2)) 174 | ) * options.animation; 175 | } 176 | -------------------------------------------------------------------------------- /src/BrowserInfo.js: -------------------------------------------------------------------------------- 1 | function userAgent(pattern) { 2 | if (typeof window !== 'undefined' && window.navigator) { 3 | return !!/*@__PURE__*/navigator.userAgent.match(pattern); 4 | } 5 | } 6 | 7 | export const IE11OrLess = userAgent(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i); 8 | export const Edge = userAgent(/Edge/i); 9 | export const FireFox = userAgent(/firefox/i); 10 | export const Safari = userAgent(/safari/i) && !userAgent(/chrome/i) && !userAgent(/android/i); 11 | export const IOS = userAgent(/iP(ad|od|hone)/i); 12 | export const ChromeForAndroid = userAgent(/chrome/i) && userAgent(/android/i); 13 | -------------------------------------------------------------------------------- /src/EventDispatcher.js: -------------------------------------------------------------------------------- 1 | import { IE11OrLess, Edge } from './BrowserInfo.js'; 2 | import { expando } from './utils.js'; 3 | import PluginManager from './PluginManager.js'; 4 | 5 | export default function dispatchEvent( 6 | { 7 | sortable, rootEl, name, 8 | targetEl, cloneEl, toEl, fromEl, 9 | oldIndex, newIndex, 10 | oldDraggableIndex, newDraggableIndex, 11 | originalEvent, putSortable, extraEventProperties 12 | } 13 | ) { 14 | sortable = (sortable || (rootEl && rootEl[expando])); 15 | if (!sortable) return; 16 | 17 | let evt, 18 | options = sortable.options, 19 | onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); 20 | // Support for new CustomEvent feature 21 | if (window.CustomEvent && !IE11OrLess && !Edge) { 22 | evt = new CustomEvent(name, { 23 | bubbles: true, 24 | cancelable: true 25 | }); 26 | } else { 27 | evt = document.createEvent('Event'); 28 | evt.initEvent(name, true, true); 29 | } 30 | 31 | evt.to = toEl || rootEl; 32 | evt.from = fromEl || rootEl; 33 | evt.item = targetEl || rootEl; 34 | evt.clone = cloneEl; 35 | 36 | evt.oldIndex = oldIndex; 37 | evt.newIndex = newIndex; 38 | 39 | evt.oldDraggableIndex = oldDraggableIndex; 40 | evt.newDraggableIndex = newDraggableIndex; 41 | 42 | evt.originalEvent = originalEvent; 43 | evt.pullMode = putSortable ? putSortable.lastPutMode : undefined; 44 | 45 | let allEventProperties = { ...extraEventProperties, ...PluginManager.getEventProperties(name, sortable) }; 46 | for (let option in allEventProperties) { 47 | evt[option] = allEventProperties[option]; 48 | } 49 | 50 | if (rootEl) { 51 | rootEl.dispatchEvent(evt); 52 | } 53 | 54 | if (options[onName]) { 55 | options[onName].call(sortable, evt); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/PluginManager.js: -------------------------------------------------------------------------------- 1 | let plugins = []; 2 | 3 | const defaults = { 4 | initializeByDefault: true 5 | }; 6 | 7 | export default { 8 | mount(plugin) { 9 | // Set default static properties 10 | for (let option in defaults) { 11 | if (defaults.hasOwnProperty(option) && !(option in plugin)) { 12 | plugin[option] = defaults[option]; 13 | } 14 | } 15 | 16 | plugins.forEach(p => { 17 | if (p.pluginName === plugin.pluginName) { 18 | throw (`Sortable: Cannot mount plugin ${ plugin.pluginName } more than once`); 19 | } 20 | }); 21 | 22 | plugins.push(plugin); 23 | }, 24 | pluginEvent(eventName, sortable, evt) { 25 | this.eventCanceled = false; 26 | evt.cancel = () => { 27 | this.eventCanceled = true; 28 | }; 29 | const eventNameGlobal = eventName + 'Global'; 30 | plugins.forEach(plugin => { 31 | if (!sortable[plugin.pluginName]) return; 32 | // Fire global events if it exists in this sortable 33 | if ( 34 | sortable[plugin.pluginName][eventNameGlobal] 35 | ) { 36 | sortable[plugin.pluginName][eventNameGlobal]({ sortable, ...evt }); 37 | } 38 | 39 | // Only fire plugin event if plugin is enabled in this sortable, 40 | // and plugin has event defined 41 | if ( 42 | sortable.options[plugin.pluginName] && 43 | sortable[plugin.pluginName][eventName] 44 | ) { 45 | sortable[plugin.pluginName][eventName]({ sortable, ...evt }); 46 | } 47 | }); 48 | }, 49 | initializePlugins(sortable, el, defaults, options) { 50 | plugins.forEach(plugin => { 51 | const pluginName = plugin.pluginName; 52 | if (!sortable.options[pluginName] && !plugin.initializeByDefault) return; 53 | 54 | let initialized = new plugin(sortable, el, sortable.options); 55 | initialized.sortable = sortable; 56 | initialized.options = sortable.options; 57 | sortable[pluginName] = initialized; 58 | 59 | // Add default options from plugin 60 | Object.assign(defaults, initialized.defaults); 61 | }); 62 | 63 | for (let option in sortable.options) { 64 | if (!sortable.options.hasOwnProperty(option)) continue; 65 | let modified = this.modifyOption(sortable, option, sortable.options[option]); 66 | if (typeof(modified) !== 'undefined') { 67 | sortable.options[option] = modified; 68 | } 69 | } 70 | }, 71 | getEventProperties(name, sortable) { 72 | let eventProperties = {}; 73 | plugins.forEach(plugin => { 74 | if (typeof(plugin.eventProperties) !== 'function') return; 75 | Object.assign(eventProperties, plugin.eventProperties.call(sortable[plugin.pluginName], name)); 76 | }); 77 | 78 | return eventProperties; 79 | }, 80 | modifyOption(sortable, name, value) { 81 | let modifiedValue; 82 | plugins.forEach(plugin => { 83 | // Plugin must exist on the Sortable 84 | if (!sortable[plugin.pluginName]) return; 85 | 86 | // If static option listener exists for this option, call in the context of the Sortable's instance of this plugin 87 | if (plugin.optionListeners && typeof(plugin.optionListeners[name]) === 'function') { 88 | modifiedValue = plugin.optionListeners[name].call(sortable[plugin.pluginName], value); 89 | } 90 | }); 91 | 92 | return modifiedValue; 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { IE11OrLess } from './BrowserInfo.js'; 2 | import Sortable from './Sortable.js'; 3 | 4 | const captureMode = { 5 | capture: false, 6 | passive: false 7 | }; 8 | 9 | function on(el, event, fn) { 10 | el.addEventListener(event, fn, !IE11OrLess && captureMode); 11 | } 12 | 13 | 14 | function off(el, event, fn) { 15 | el.removeEventListener(event, fn, !IE11OrLess && captureMode); 16 | } 17 | 18 | function matches(/**HTMLElement*/el, /**String*/selector) { 19 | if (!selector) return; 20 | 21 | selector[0] === '>' && (selector = selector.substring(1)); 22 | 23 | if (el) { 24 | try { 25 | if (el.matches) { 26 | return el.matches(selector); 27 | } else if (el.msMatchesSelector) { 28 | return el.msMatchesSelector(selector); 29 | } else if (el.webkitMatchesSelector) { 30 | return el.webkitMatchesSelector(selector); 31 | } 32 | } catch(_) { 33 | return false; 34 | } 35 | } 36 | 37 | return false; 38 | } 39 | 40 | function getParentOrHost(el) { 41 | return (el.host && el !== document && el.host.nodeType && el.host !== el) 42 | ? el.host 43 | : el.parentNode; 44 | } 45 | 46 | function closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx, includeCTX) { 47 | if (el) { 48 | ctx = ctx || document; 49 | 50 | do { 51 | if ( 52 | selector != null && 53 | ( 54 | selector[0] === '>' ? 55 | el.parentNode === ctx && matches(el, selector) : 56 | matches(el, selector) 57 | ) || 58 | includeCTX && el === ctx 59 | ) { 60 | return el; 61 | } 62 | 63 | if (el === ctx) break; 64 | /* jshint boss:true */ 65 | } while (el = getParentOrHost(el)); 66 | } 67 | 68 | return null; 69 | } 70 | 71 | const R_SPACE = /\s+/g; 72 | 73 | function toggleClass(el, name, state) { 74 | if (el && name) { 75 | if (el.classList) { 76 | el.classList[state ? 'add' : 'remove'](name); 77 | } 78 | else { 79 | let className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' '); 80 | el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' '); 81 | } 82 | } 83 | } 84 | 85 | 86 | function css(el, prop, val) { 87 | let style = el && el.style; 88 | 89 | if (style) { 90 | if (val === void 0) { 91 | if (document.defaultView && document.defaultView.getComputedStyle) { 92 | val = document.defaultView.getComputedStyle(el, ''); 93 | } 94 | else if (el.currentStyle) { 95 | val = el.currentStyle; 96 | } 97 | 98 | return prop === void 0 ? val : val[prop]; 99 | } 100 | else { 101 | if (!(prop in style) && prop.indexOf('webkit') === -1) { 102 | prop = '-webkit-' + prop; 103 | } 104 | 105 | style[prop] = val + (typeof val === 'string' ? '' : 'px'); 106 | } 107 | } 108 | } 109 | 110 | function matrix(el, selfOnly) { 111 | let appliedTransforms = ''; 112 | if (typeof(el) === 'string') { 113 | appliedTransforms = el; 114 | } else { 115 | do { 116 | let transform = css(el, 'transform'); 117 | 118 | if (transform && transform !== 'none') { 119 | appliedTransforms = transform + ' ' + appliedTransforms; 120 | } 121 | /* jshint boss:true */ 122 | } while (!selfOnly && (el = el.parentNode)); 123 | } 124 | 125 | const matrixFn = window.DOMMatrix || window.WebKitCSSMatrix || window.CSSMatrix || window.MSCSSMatrix; 126 | /*jshint -W056 */ 127 | return matrixFn && (new matrixFn(appliedTransforms)); 128 | } 129 | 130 | 131 | function find(ctx, tagName, iterator) { 132 | if (ctx) { 133 | let list = ctx.getElementsByTagName(tagName), i = 0, n = list.length; 134 | 135 | if (iterator) { 136 | for (; i < n; i++) { 137 | iterator(list[i], i); 138 | } 139 | } 140 | 141 | return list; 142 | } 143 | 144 | return []; 145 | } 146 | 147 | 148 | 149 | function getWindowScrollingElement() { 150 | let scrollingElement = document.scrollingElement; 151 | 152 | if (scrollingElement) { 153 | return scrollingElement 154 | } else { 155 | return document.documentElement 156 | } 157 | } 158 | 159 | 160 | /** 161 | * Returns the "bounding client rect" of given element 162 | * @param {HTMLElement} el The element whose boundingClientRect is wanted 163 | * @param {[Boolean]} relativeToContainingBlock Whether the rect should be relative to the containing block of (including) the container 164 | * @param {[Boolean]} relativeToNonStaticParent Whether the rect should be relative to the relative parent of (including) the contaienr 165 | * @param {[Boolean]} undoScale Whether the container's scale() should be undone 166 | * @param {[HTMLElement]} container The parent the element will be placed in 167 | * @return {Object} The boundingClientRect of el, with specified adjustments 168 | */ 169 | function getRect(el, relativeToContainingBlock, relativeToNonStaticParent, undoScale, container) { 170 | if (!el.getBoundingClientRect && el !== window) return; 171 | 172 | let elRect, 173 | top, 174 | left, 175 | bottom, 176 | right, 177 | height, 178 | width; 179 | 180 | if (el !== window && el.parentNode && el !== getWindowScrollingElement()) { 181 | elRect = el.getBoundingClientRect(); 182 | top = elRect.top; 183 | left = elRect.left; 184 | bottom = elRect.bottom; 185 | right = elRect.right; 186 | height = elRect.height; 187 | width = elRect.width; 188 | } else { 189 | top = 0; 190 | left = 0; 191 | bottom = window.innerHeight; 192 | right = window.innerWidth; 193 | height = window.innerHeight; 194 | width = window.innerWidth; 195 | } 196 | 197 | if ((relativeToContainingBlock || relativeToNonStaticParent) && el !== window) { 198 | // Adjust for translate() 199 | container = container || el.parentNode; 200 | 201 | // solves #1123 (see: https://stackoverflow.com/a/37953806/6088312) 202 | // Not needed on <= IE11 203 | if (!IE11OrLess) { 204 | do { 205 | if ( 206 | container && 207 | container.getBoundingClientRect && 208 | ( 209 | css(container, 'transform') !== 'none' || 210 | relativeToNonStaticParent && 211 | css(container, 'position') !== 'static' 212 | ) 213 | ) { 214 | let containerRect = container.getBoundingClientRect(); 215 | 216 | // Set relative to edges of padding box of container 217 | top -= containerRect.top + parseInt(css(container, 'border-top-width')); 218 | left -= containerRect.left + parseInt(css(container, 'border-left-width')); 219 | bottom = top + elRect.height; 220 | right = left + elRect.width; 221 | 222 | break; 223 | } 224 | /* jshint boss:true */ 225 | } while (container = container.parentNode); 226 | } 227 | } 228 | 229 | if (undoScale && el !== window) { 230 | // Adjust for scale() 231 | let elMatrix = matrix(container || el), 232 | scaleX = elMatrix && elMatrix.a, 233 | scaleY = elMatrix && elMatrix.d; 234 | 235 | if (elMatrix) { 236 | top /= scaleY; 237 | left /= scaleX; 238 | 239 | width /= scaleX; 240 | height /= scaleY; 241 | 242 | bottom = top + height; 243 | right = left + width; 244 | } 245 | } 246 | 247 | return { 248 | top: top, 249 | left: left, 250 | bottom: bottom, 251 | right: right, 252 | width: width, 253 | height: height 254 | }; 255 | } 256 | 257 | /** 258 | * Returns the content rect of the element (bounding rect minus border and padding) 259 | * @param {HTMLElement} el 260 | */ 261 | function getContentRect(el) { 262 | let rect = getRect(el); 263 | const paddingLeft = parseInt(css(el, 'padding-left')), 264 | paddingTop = parseInt(css(el, 'padding-top')), 265 | paddingRight = parseInt(css(el, 'padding-right')), 266 | paddingBottom = parseInt(css(el, 'padding-bottom')); 267 | rect.top += paddingTop + parseInt(css(el, 'border-top-width')); 268 | rect.left += paddingLeft + parseInt(css(el, 'border-left-width')); 269 | // Client Width/Height includes padding only 270 | rect.width = el.clientWidth - paddingLeft - paddingRight; 271 | rect.height = el.clientHeight - paddingTop - paddingBottom; 272 | rect.bottom = rect.top + rect.height; 273 | rect.right = rect.left + rect.width; 274 | return rect; 275 | } 276 | 277 | /** 278 | * Checks if a side of an element is scrolled past a side of its parents 279 | * @param {HTMLElement} el The element who's side being scrolled out of view is in question 280 | * @param {String} elSide Side of the element in question ('top', 'left', 'right', 'bottom') 281 | * @param {String} parentSide Side of the parent in question ('top', 'left', 'right', 'bottom') 282 | * @return {HTMLElement} The parent scroll element that the el's side is scrolled past, or null if there is no such element 283 | */ 284 | function isScrolledPast(el, elSide, parentSide) { 285 | let parent = getParentAutoScrollElement(el, true), 286 | elSideVal = getRect(el)[elSide]; 287 | 288 | /* jshint boss:true */ 289 | while (parent) { 290 | let parentSideVal = getRect(parent)[parentSide], 291 | visible; 292 | 293 | if (parentSide === 'top' || parentSide === 'left') { 294 | visible = elSideVal >= parentSideVal; 295 | } else { 296 | visible = elSideVal <= parentSideVal; 297 | } 298 | 299 | if (!visible) return parent; 300 | 301 | if (parent === getWindowScrollingElement()) break; 302 | 303 | parent = getParentAutoScrollElement(parent, false); 304 | } 305 | 306 | return false; 307 | } 308 | 309 | 310 | 311 | /** 312 | * Gets nth child of el, ignoring hidden children, sortable's elements (does not ignore clone if it's visible) 313 | * and non-draggable elements 314 | * @param {HTMLElement} el The parent element 315 | * @param {Number} childNum The index of the child 316 | * @param {Object} options Parent Sortable's options 317 | * @return {HTMLElement} The child at index childNum, or null if not found 318 | */ 319 | function getChild(el, childNum, options, includeDragEl) { 320 | let currentChild = 0, 321 | i = 0, 322 | children = el.children; 323 | 324 | while (i < children.length) { 325 | if ( 326 | children[i].style.display !== 'none' && 327 | children[i] !== Sortable.ghost && 328 | (includeDragEl || children[i] !== Sortable.dragged) && 329 | closest(children[i], options.draggable, el, false) 330 | ) { 331 | if (currentChild === childNum) { 332 | return children[i]; 333 | } 334 | currentChild++; 335 | } 336 | 337 | i++; 338 | } 339 | return null; 340 | } 341 | 342 | /** 343 | * Gets the last child in the el, ignoring ghostEl or invisible elements (clones) 344 | * @param {HTMLElement} el Parent element 345 | * @param {selector} selector Any other elements that should be ignored 346 | * @return {HTMLElement} The last child, ignoring ghostEl 347 | */ 348 | function lastChild(el, selector) { 349 | let last = el.lastElementChild; 350 | 351 | while ( 352 | last && 353 | ( 354 | last === Sortable.ghost || 355 | css(last, 'display') === 'none' || 356 | selector && !matches(last, selector) 357 | ) 358 | ) { 359 | last = last.previousElementSibling; 360 | } 361 | 362 | return last || null; 363 | } 364 | 365 | 366 | /** 367 | * Returns the index of an element within its parent for a selected set of 368 | * elements 369 | * @param {HTMLElement} el 370 | * @param {selector} selector 371 | * @return {number} 372 | */ 373 | function index(el, selector) { 374 | let index = 0; 375 | 376 | if (!el || !el.parentNode) { 377 | return -1; 378 | } 379 | 380 | /* jshint boss:true */ 381 | while (el = el.previousElementSibling) { 382 | if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && el !== Sortable.clone && (!selector || matches(el, selector))) { 383 | index++; 384 | } 385 | } 386 | 387 | return index; 388 | } 389 | 390 | /** 391 | * Returns the scroll offset of the given element, added with all the scroll offsets of parent elements. 392 | * The value is returned in real pixels. 393 | * @param {HTMLElement} el 394 | * @return {Array} Offsets in the format of [left, top] 395 | */ 396 | function getRelativeScrollOffset(el) { 397 | let offsetLeft = 0, 398 | offsetTop = 0, 399 | winScroller = getWindowScrollingElement(); 400 | 401 | if (el) { 402 | do { 403 | let elMatrix = matrix(el), 404 | scaleX = elMatrix.a, 405 | scaleY = elMatrix.d; 406 | 407 | offsetLeft += el.scrollLeft * scaleX; 408 | offsetTop += el.scrollTop * scaleY; 409 | } while (el !== winScroller && (el = el.parentNode)); 410 | } 411 | 412 | return [offsetLeft, offsetTop]; 413 | } 414 | 415 | /** 416 | * Returns the index of the object within the given array 417 | * @param {Array} arr Array that may or may not hold the object 418 | * @param {Object} obj An object that has a key-value pair unique to and identical to a key-value pair in the object you want to find 419 | * @return {Number} The index of the object in the array, or -1 420 | */ 421 | function indexOfObject(arr, obj) { 422 | for (let i in arr) { 423 | if (!arr.hasOwnProperty(i)) continue; 424 | for (let key in obj) { 425 | if (obj.hasOwnProperty(key) && obj[key] === arr[i][key]) return Number(i); 426 | } 427 | } 428 | return -1; 429 | } 430 | 431 | 432 | function getParentAutoScrollElement(el, includeSelf) { 433 | // skip to window 434 | if (!el || !el.getBoundingClientRect) return getWindowScrollingElement(); 435 | 436 | let elem = el; 437 | let gotSelf = false; 438 | do { 439 | // we don't need to get elem css if it isn't even overflowing in the first place (performance) 440 | if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) { 441 | let elemCSS = css(elem); 442 | if ( 443 | elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == 'auto' || elemCSS.overflowX == 'scroll') || 444 | elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == 'auto' || elemCSS.overflowY == 'scroll') 445 | ) { 446 | if (!elem.getBoundingClientRect || elem === document.body) return getWindowScrollingElement(); 447 | 448 | if (gotSelf || includeSelf) return elem; 449 | gotSelf = true; 450 | } 451 | } 452 | /* jshint boss:true */ 453 | } while (elem = elem.parentNode); 454 | 455 | return getWindowScrollingElement(); 456 | } 457 | 458 | function extend(dst, src) { 459 | if (dst && src) { 460 | for (let key in src) { 461 | if (src.hasOwnProperty(key)) { 462 | dst[key] = src[key]; 463 | } 464 | } 465 | } 466 | 467 | return dst; 468 | } 469 | 470 | 471 | function isRectEqual(rect1, rect2) { 472 | return Math.round(rect1.top) === Math.round(rect2.top) && 473 | Math.round(rect1.left) === Math.round(rect2.left) && 474 | Math.round(rect1.height) === Math.round(rect2.height) && 475 | Math.round(rect1.width) === Math.round(rect2.width); 476 | } 477 | 478 | 479 | let _throttleTimeout; 480 | function throttle(callback, ms) { 481 | return function () { 482 | if (!_throttleTimeout) { 483 | let args = arguments, 484 | _this = this; 485 | 486 | if (args.length === 1) { 487 | callback.call(_this, args[0]); 488 | } else { 489 | callback.apply(_this, args); 490 | } 491 | 492 | _throttleTimeout = setTimeout(function () { 493 | _throttleTimeout = void 0; 494 | }, ms); 495 | } 496 | }; 497 | } 498 | 499 | 500 | function cancelThrottle() { 501 | clearTimeout(_throttleTimeout); 502 | _throttleTimeout = void 0; 503 | } 504 | 505 | 506 | function scrollBy(el, x, y) { 507 | el.scrollLeft += x; 508 | el.scrollTop += y; 509 | } 510 | 511 | 512 | function clone(el) { 513 | let Polymer = window.Polymer; 514 | let $ = window.jQuery || window.Zepto; 515 | 516 | if (Polymer && Polymer.dom) { 517 | return Polymer.dom(el).cloneNode(true); 518 | } 519 | else if ($) { 520 | return $(el).clone(true)[0]; 521 | } 522 | else { 523 | return el.cloneNode(true); 524 | } 525 | } 526 | 527 | 528 | function setRect(el, rect) { 529 | css(el, 'position', 'absolute'); 530 | css(el, 'top', rect.top); 531 | css(el, 'left', rect.left); 532 | css(el, 'width', rect.width); 533 | css(el, 'height', rect.height); 534 | } 535 | 536 | function unsetRect(el) { 537 | css(el, 'position', ''); 538 | css(el, 'top', ''); 539 | css(el, 'left', ''); 540 | css(el, 'width', ''); 541 | css(el, 'height', ''); 542 | } 543 | 544 | function getChildContainingRectFromElement(container, options, ghostEl) { 545 | const rect = {}; 546 | 547 | Array.from(container.children).forEach(child => { 548 | if (!closest(child, options.draggable, container, false) || child.animated || child === ghostEl) return; 549 | const childRect = getRect(child); 550 | rect.left = Math.min(rect.left ?? Infinity, childRect.left); 551 | rect.top = Math.min(rect.top ?? Infinity, childRect.top); 552 | rect.right = Math.max(rect.right ?? -Infinity, childRect.right); 553 | rect.bottom = Math.max(rect.bottom ?? -Infinity, childRect.bottom); 554 | }); 555 | rect.width = rect.right - rect.left; 556 | rect.height = rect.bottom - rect.top; 557 | rect.x = rect.left; 558 | rect.y = rect.top; 559 | return rect; 560 | } 561 | 562 | const expando = 'Sortable' + (new Date).getTime(); 563 | 564 | 565 | export { 566 | on, 567 | off, 568 | matches, 569 | getParentOrHost, 570 | closest, 571 | toggleClass, 572 | css, 573 | matrix, 574 | find, 575 | getWindowScrollingElement, 576 | getRect, 577 | isScrolledPast, 578 | getChild, 579 | lastChild, 580 | index, 581 | getRelativeScrollOffset, 582 | indexOfObject, 583 | getParentAutoScrollElement, 584 | extend, 585 | isRectEqual, 586 | throttle, 587 | cancelThrottle, 588 | scrollBy, 589 | clone, 590 | setRect, 591 | unsetRect, 592 | getContentRect, 593 | getChildContainingRectFromElement, 594 | expando 595 | }; 596 | -------------------------------------------------------------------------------- /st/app.js: -------------------------------------------------------------------------------- 1 | var example1 = document.getElementById('example1'), 2 | example2Left = document.getElementById('example2-left'), 3 | example2Right = document.getElementById('example2-right'), 4 | example3Left = document.getElementById('example3-left'), 5 | example3Right = document.getElementById('example3-right'), 6 | example4Left = document.getElementById('example4-left'), 7 | example4Right = document.getElementById('example4-right'), 8 | example5 = document.getElementById('example5'), 9 | example6 = document.getElementById('example6'), 10 | example7 = document.getElementById('example7'), 11 | gridDemo = document.getElementById('gridDemo'), 12 | multiDragDemo = document.getElementById('multiDragDemo'), 13 | swapDemo = document.getElementById('swapDemo'); 14 | 15 | // Example 1 - Simple list 16 | new Sortable(example1, { 17 | animation: 150, 18 | ghostClass: 'blue-background-class' 19 | }); 20 | 21 | 22 | // Example 2 - Shared lists 23 | new Sortable(example2Left, { 24 | group: 'shared', // set both lists to same group 25 | animation: 150 26 | }); 27 | 28 | new Sortable(example2Right, { 29 | group: 'shared', 30 | animation: 150 31 | }); 32 | 33 | // Example 3 - Cloning 34 | new Sortable(example3Left, { 35 | group: { 36 | name: 'shared', 37 | pull: 'clone' // To clone: set pull to 'clone' 38 | }, 39 | animation: 150 40 | }); 41 | 42 | new Sortable(example3Right, { 43 | group: { 44 | name: 'shared', 45 | pull: 'clone' 46 | }, 47 | animation: 150 48 | }); 49 | 50 | 51 | // Example 4 - No Sorting 52 | new Sortable(example4Left, { 53 | group: { 54 | name: 'shared', 55 | pull: 'clone', 56 | put: false // Do not allow items to be put into this list 57 | }, 58 | animation: 150, 59 | sort: false // To disable sorting: set sort to false 60 | }); 61 | 62 | new Sortable(example4Right, { 63 | group: 'shared', 64 | animation: 150 65 | }); 66 | 67 | 68 | // Example 5 - Handle 69 | new Sortable(example5, { 70 | handle: '.handle', // handle class 71 | animation: 150 72 | }); 73 | 74 | // Example 6 - Filter 75 | new Sortable(example6, { 76 | filter: '.filtered', 77 | animation: 150 78 | }); 79 | 80 | // Example 7 - Thresholds 81 | var example7Sortable = new Sortable(example7, { 82 | animation: 150 83 | }); 84 | 85 | 86 | var example7SwapThreshold = 1; 87 | var example7SwapThresholdInput = document.getElementById('example7SwapThresholdInput'); 88 | var example7SwapThresholdCode = document.getElementById('example7SwapThresholdCode'); 89 | var example7SwapThresholdIndicators = [].slice.call(document.querySelectorAll('.swap-threshold-indicator')); 90 | 91 | var example7InvertSwapInput = document.getElementById('example7InvertSwapInput'); 92 | var example7InvertSwapCode = document.getElementById('example7InvertSwapCode'); 93 | var example7InvertedSwapThresholdIndicators = [].slice.call(document.querySelectorAll('.inverted-swap-threshold-indicator')); 94 | 95 | var example7Squares = [].slice.call(document.querySelectorAll('.square')); 96 | 97 | var activeIndicators = example7SwapThresholdIndicators; 98 | 99 | var example7DirectionInput = document.getElementById('example7DirectionInput'); 100 | var example7SizeProperty = 'width'; 101 | 102 | 103 | function renderThresholdWidth(evt) { 104 | example7SwapThreshold = Number(evt.target.value); 105 | example7SwapThresholdCode.innerHTML = evt.target.value.indexOf('.') > -1 ? (evt.target.value + '0000').slice(0, 4) : evt.target.value; 106 | 107 | for (var i = 0; i < activeIndicators.length; i++) { 108 | activeIndicators[i].style[example7SizeProperty] = (evt.target.value * 100) / 109 | (activeIndicators == example7SwapThresholdIndicators ? 1 : 2) + '%'; 110 | } 111 | 112 | example7Sortable.option('swapThreshold', example7SwapThreshold); 113 | } 114 | 115 | example7SwapThresholdInput.addEventListener('input', renderThresholdWidth); 116 | example7SwapThresholdInput.addEventListener('change', renderThresholdWidth); 117 | 118 | example7InvertSwapInput.addEventListener('change', function(evt) { 119 | example7Sortable.option('invertSwap', evt.target.checked); 120 | 121 | 122 | for (var i = 0; i < activeIndicators.length; i++) { 123 | activeIndicators[i].style.display = 'none'; 124 | } 125 | 126 | if (evt.target.checked) { 127 | 128 | example7InvertSwapCode.style.display = ''; 129 | 130 | activeIndicators = example7InvertedSwapThresholdIndicators; 131 | } else { 132 | example7InvertSwapCode.style.display = 'none'; 133 | activeIndicators = example7SwapThresholdIndicators; 134 | } 135 | 136 | renderThresholdWidth({ 137 | target: example7SwapThresholdInput 138 | }); 139 | 140 | for (i = 0; i < activeIndicators.length; i++) { 141 | activeIndicators[i].style.display = ''; 142 | } 143 | }); 144 | 145 | function renderDirection(evt) { 146 | for (var i = 0; i < example7Squares.length; i++) { 147 | example7Squares[i].style.display = evt.target.value === 'h' ? 'inline-block' : 'block'; 148 | } 149 | 150 | for (i = 0; i < example7InvertedSwapThresholdIndicators.length; i++) { 151 | /* jshint expr:true */ 152 | evt.target.value === 'h' && (example7InvertedSwapThresholdIndicators[i].style.height = '100%'); 153 | evt.target.value === 'v' && (example7InvertedSwapThresholdIndicators[i].style.width = '100%'); 154 | } 155 | 156 | for (i = 0; i < example7SwapThresholdIndicators.length; i++) { 157 | if (evt.target.value === 'h') { 158 | example7SwapThresholdIndicators[i].style.height = '100%'; 159 | example7SwapThresholdIndicators[i].style.marginLeft = '50%'; 160 | example7SwapThresholdIndicators[i].style.transform = 'translateX(-50%)'; 161 | 162 | example7SwapThresholdIndicators[i].style.marginTop = '0'; 163 | } else { 164 | example7SwapThresholdIndicators[i].style.width = '100%'; 165 | example7SwapThresholdIndicators[i].style.marginTop = '50%'; 166 | example7SwapThresholdIndicators[i].style.transform = 'translateY(-50%)'; 167 | 168 | example7SwapThresholdIndicators[i].style.marginLeft = '0'; 169 | } 170 | } 171 | 172 | if (evt.target.value === 'h') { 173 | example7SizeProperty = 'width'; 174 | example7Sortable.option('direction', 'horizontal'); 175 | } else { 176 | example7SizeProperty = 'height'; 177 | example7Sortable.option('direction', 'vertical'); 178 | } 179 | 180 | renderThresholdWidth({ 181 | target: example7SwapThresholdInput 182 | }); 183 | } 184 | example7DirectionInput.addEventListener('change', renderDirection); 185 | 186 | renderDirection({ 187 | target: example7DirectionInput 188 | }); 189 | 190 | 191 | // Grid demo 192 | new Sortable(gridDemo, { 193 | animation: 150, 194 | ghostClass: 'blue-background-class' 195 | }); 196 | 197 | // Nested demo 198 | var nestedSortables = [].slice.call(document.querySelectorAll('.nested-sortable')); 199 | 200 | // Loop through each nested sortable element 201 | for (var i = 0; i < nestedSortables.length; i++) { 202 | new Sortable(nestedSortables[i], { 203 | group: 'nested', 204 | animation: 150, 205 | fallbackOnBody: true, 206 | swapThreshold: 0.65 207 | }); 208 | } 209 | 210 | // MultiDrag demo 211 | new Sortable(multiDragDemo, { 212 | multiDrag: true, 213 | selectedClass: 'selected', 214 | fallbackTolerance: 3, // So that we can select items on mobile 215 | animation: 150 216 | }); 217 | 218 | 219 | // Swap demo 220 | new Sortable(swapDemo, { 221 | swap: true, 222 | swapClass: 'highlight', 223 | animation: 150 224 | }); 225 | -------------------------------------------------------------------------------- /st/iframe/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 | 14 16 | 17 | Drag me by the handle 18 |
    19 |
    20 | 2 21 | 22 | You can also select text 23 |
    24 |
    25 | 1 26 | 27 | Best of both worlds! 28 |
    29 |
    30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /st/iframe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IFrame playground 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
    20 |
    This is Sortable
    21 |
    It works with Bootstrap...
    22 |
    ...out of the box.
    23 |
    It has support for touch devices.
    24 |
    Just drag some elements around.
    25 |
    26 | 27 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /st/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SortableJS/Sortable/3696da44b57c81dbe441aa48af28b0c38f6b11e2/st/logo.png -------------------------------------------------------------------------------- /st/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SortableJS/Sortable/3696da44b57c81dbe441aa48af28b0c38f6b11e2/st/og-image.png -------------------------------------------------------------------------------- /st/prettify/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.clo,.opn,.pun{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.kwd,.tag,.typ{font-weight:700}.str{color:#060}.kwd{color:#006}.com{color:#600;font-style:italic}.typ{color:#404}.lit{color:#044}.clo,.opn,.pun{color:#440}.tag{color:#006}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} -------------------------------------------------------------------------------- /st/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | !function(){/* 2 | 3 | Copyright (C) 2006 Google Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | "undefined"!==typeof window&&(window.PR_SHOULD_USE_CONTINUATION=!0); 18 | (function(){function T(a){function d(e){var a=e.charCodeAt(0);if(92!==a)return a;var c=e.charAt(1);return(a=w[c])?a:"0"<=c&&"7">=c?parseInt(e.substring(1),8):"u"===c||"x"===c?parseInt(e.substring(2),16):e.charCodeAt(1)}function f(e){if(32>e)return(16>e?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e);return"\\"===e||"-"===e||"]"===e||"^"===e?"\\"+e:e}function c(e){var c=e.substring(1,e.length-1).match(RegExp("\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]","g")); 19 | e=[];var a="^"===c[0],b=["["];a&&b.push("^");for(var a=a?1:0,g=c.length;ak||122k||90k||122h[0]&&(h[1]+1>h[0]&&b.push("-"),b.push(f(h[1])));b.push("]");return b.join("")}function m(e){for(var a=e.source.match(RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g")),b=a.length,d=[],g=0,h=0;g/,null])):d.push(["com",/^#[^\r\n]*/,null,"#"]));a.cStyleComments&&(f.push(["com",/^\/\/[^\r\n]*/,null]),f.push(["com",/^\/\*[\s\S]*?(?:\*\/|$)/,null]));if(c=a.regexLiterals){var m=(c=1|\\/=?|::?|<>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+ 28 | ("/(?=[^/*"+c+"])(?:[^/\\x5B\\x5C"+c+"]|\\x5C"+m+"|\\x5B(?:[^\\x5C\\x5D"+c+"]|\\x5C"+m+")*(?:\\x5D|$))+/")+")")])}(c=a.types)&&f.push(["typ",c]);c=(""+a.keywords).replace(/^ | $/g,"");c.length&&f.push(["kwd",new RegExp("^(?:"+c.replace(/[\s,]+/g,"|")+")\\b"),null]);d.push(["pln",/^\s+/,null," \r\n\t\u00a0"]);c="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(c+="(?!s*/)");f.push(["lit",/^@[a-z_$][a-z_$@0-9]*/i,null],["typ",/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],["pln",/^[a-z_$][a-z_$@0-9]*/i, 29 | null],["lit",/^(?:0x[a-f0-9]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+\-]?\d+)?)[a-z]*/i,null,"0123456789"],["pln",/^\\[\s\S]?/,null],["pun",new RegExp(c),null]);return G(d,f)}function L(a,d,f){function c(a){var b=a.nodeType;if(1==b&&!t.test(a.className))if("br"===a.nodeName.toLowerCase())m(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)c(a);else if((3==b||4==b)&&f){var e=a.nodeValue,d=e.match(q);d&&(b=e.substring(0,d.index),a.nodeValue=b,(e=e.substring(d.index+ 30 | d[0].length))&&a.parentNode.insertBefore(l.createTextNode(e),a.nextSibling),m(a),b||a.parentNode.removeChild(a))}}function m(a){function c(a,b){var e=b?a.cloneNode(!1):a,k=a.parentNode;if(k){var k=c(k,1),d=a.nextSibling;k.appendChild(e);for(var f=d;f;f=d)d=f.nextSibling,k.appendChild(f)}return e}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;a=c(a.nextSibling,0);for(var e;(e=a.parentNode)&&1===e.nodeType;)a=e;b.push(a)}for(var t=/(?:^|\s)nocode(?:\s|$)/,q=/\r\n?|\n/,l=a.ownerDocument,n=l.createElement("li");a.firstChild;)n.appendChild(a.firstChild); 31 | for(var b=[n],p=0;p=+m[1],d=/\n/g,t=a.a,q=t.length,f=0,l=a.c,n=l.length,c=0,b=a.g,p=b.length,w=0;b[p]=q;var r,e;for(e=r=0;e=h&&(c+=2);f>=k&&(w+=2)}}finally{g&&(g.style.display=a)}}catch(y){D.console&&console.log(y&&y.stack||y)}}var D="undefined"!==typeof window? 34 | window:{},B=["break,continue,do,else,for,if,return,while"],F=[[B,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,restrict,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],H=[F,"alignas,alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,noexcept,noreturn,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"], 35 | O=[F,"abstract,assert,boolean,byte,extends,finally,final,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],P=[F,"abstract,add,alias,as,ascending,async,await,base,bool,by,byte,checked,decimal,delegate,descending,dynamic,event,finally,fixed,foreach,from,get,global,group,implicit,in,interface,internal,into,is,join,let,lock,null,object,out,override,orderby,params,partial,readonly,ref,remove,sbyte,sealed,select,set,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,value,var,virtual,where,yield"], 36 | F=[F,"abstract,async,await,constructor,debugger,enum,eval,export,from,function,get,import,implements,instanceof,interface,let,null,of,set,undefined,var,with,yield,Infinity,NaN"],Q=[B,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],R=[B,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"], 37 | B=[B,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],S=/^(DIR|FILE|array|vector|(de|priority_)?queue|(forward_)?list|stack|(const_)?(reverse_)?iterator|(unordered_)?(multi)?(set|map)|bitset|u?(int|float)\d*)\b/,W=/\S/,X=x({keywords:[H,P,O,F,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",Q,R,B],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}), 38 | I={};t(X,["default-code"]);t(G([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),"default-markup htm html mxml xhtml xml xsl".split(" "));t(G([["pln",/^[\s]+/, 39 | null," \t\r\n"],["atv",/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],["pun",/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]); 40 | t(G([],[["atv",/^[\s\S]+/]]),["uq.val"]);t(x({keywords:H,hashComments:!0,cStyleComments:!0,types:S}),"c cc cpp cxx cyc m".split(" "));t(x({keywords:"null,true,false"}),["json"]);t(x({keywords:P,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:S}),["cs"]);t(x({keywords:O,cStyleComments:!0}),["java"]);t(x({keywords:B,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);t(x({keywords:Q,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);t(x({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END", 41 | hashComments:!0,multiLineStrings:!0,regexLiterals:2}),["perl","pl","pm"]);t(x({keywords:R,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);t(x({keywords:F,cStyleComments:!0,regexLiterals:!0}),["javascript","js","ts","typescript"]);t(x({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0, 42 | regexLiterals:!0}),["coffee"]);t(G([],[["str",/^[\s\S]+/]]),["regex"]);var Y=D.PR={createSimpleLexer:G,registerLangHandler:t,sourceDecorator:x,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:D.prettyPrintOne=function(a,d,f){f=f||!1;d=d||null;var c=document.createElement("div");c.innerHTML="
    "+a+"
    "; 43 | c=c.firstChild;f&&L(c,f,!0);M({j:d,m:f,h:c,l:1,a:null,i:null,c:null,g:null});return c.innerHTML},prettyPrint:D.prettyPrint=function(a,d){function f(){for(var c=D.PR_SHOULD_USE_CONTINUATION?b.now()+250:Infinity;p=c?parseInt(e.substring(1),8):"u"===c||"x"===c?parseInt(e.substring(2),16):e.charCodeAt(1)}function f(e){if(32>e)return(16>e?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e); 36 | return"\\"===e||"-"===e||"]"===e||"^"===e?"\\"+e:e}function c(e){var c=e.substring(1,e.length-1).match(RegExp("\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]","g"));e=[];var a="^"===c[0],b=["["];a&&b.push("^");for(var a=a?1:0,h=c.length;ap||122p||90p||122m[0]&&(m[1]+1>m[0]&&b.push("-"),b.push(f(m[1])));b.push("]");return b.join("")}function g(e){for(var a=e.source.match(RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)", 38 | "g")),b=a.length,d=[],h=0,m=0;h/,null])):d.push(["com",/^#[^\r\n]*/,null,"#"]));a.cStyleComments&&(f.push(["com",/^\/\/[^\r\n]*/,null]),f.push(["com",/^\/\*[\s\S]*?(?:\*\/|$)/, 45 | null]));if(c=a.regexLiterals){var g=(c=1|\\/=?|::?|<>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+("/(?=[^/*"+c+"])(?:[^/\\x5B\\x5C"+c+"]|\\x5C"+g+"|\\x5B(?:[^\\x5C\\x5D"+c+"]|\\x5C"+g+")*(?:\\x5D|$))+/")+")")])}(c=a.types)&&f.push(["typ",c]);c=(""+a.keywords).replace(/^ | $/g,"");c.length&&f.push(["kwd", 46 | new RegExp("^(?:"+c.replace(/[\s,]+/g,"|")+")\\b"),null]);d.push(["pln",/^\s+/,null," \r\n\t\u00a0"]);c="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(c+="(?!s*/)");f.push(["lit",/^@[a-z_$][a-z_$@0-9]*/i,null],["typ",/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],["pln",/^[a-z_$][a-z_$@0-9]*/i,null],["lit",/^(?:0x[a-f0-9]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+\-]?\d+)?)[a-z]*/i,null,"0123456789"],["pln",/^\\[\s\S]?/,null],["pun",new RegExp(c),null]);return E(d,f)}function B(a,d,f){function c(a){var b= 47 | a.nodeType;if(1==b&&!r.test(a.className))if("br"===a.nodeName.toLowerCase())g(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)c(a);else if((3==b||4==b)&&f){var e=a.nodeValue,d=e.match(n);d&&(b=e.substring(0,d.index),a.nodeValue=b,(e=e.substring(d.index+d[0].length))&&a.parentNode.insertBefore(q.createTextNode(e),a.nextSibling),g(a),b||a.parentNode.removeChild(a))}}function g(a){function c(a,b){var e=b?a.cloneNode(!1):a,p=a.parentNode;if(p){var p=c(p,1),d=a.nextSibling; 48 | p.appendChild(e);for(var f=d;f;f=d)d=f.nextSibling,p.appendChild(f)}return e}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;a=c(a.nextSibling,0);for(var e;(e=a.parentNode)&&1===e.nodeType;)a=e;b.push(a)}for(var r=/(?:^|\s)nocode(?:\s|$)/,n=/\r\n?|\n/,q=a.ownerDocument,k=q.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var b=[k],t=0;t=+g[1],d=/\n/g,r=a.a,k=r.length,f=0,q=a.c,n=q.length,c=0,b=a.g,t=b.length,v=0;b[t]=k;var u,e;for(e=u=0;e=m&&(c+=2);f>=p&&(v+=2)}}finally{h&&(h.style.display=a)}}catch(y){Q.console&&console.log(y&&y.stack||y)}}var Q="undefined"!==typeof window?window:{},J=["break,continue,do,else,for,if,return,while"],K=[[J,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,restrict,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 52 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],R=[K,"alignas,alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,noexcept,noreturn,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],L=[K,"abstract,assert,boolean,byte,extends,finally,final,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"], 53 | M=[K,"abstract,add,alias,as,ascending,async,await,base,bool,by,byte,checked,decimal,delegate,descending,dynamic,event,finally,fixed,foreach,from,get,global,group,implicit,in,interface,internal,into,is,join,let,lock,null,object,out,override,orderby,params,partial,readonly,ref,remove,sbyte,sealed,select,set,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,value,var,virtual,where,yield"],K=[K,"abstract,async,await,constructor,debugger,enum,eval,export,from,function,get,import,implements,instanceof,interface,let,null,of,set,undefined,var,with,yield,Infinity,NaN"], 54 | N=[J,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],O=[J,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],J=[J,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],P=/^(DIR|FILE|array|vector|(de|priority_)?queue|(forward_)?list|stack|(const_)?(reverse_)?iterator|(unordered_)?(multi)?(set|map)|bitset|u?(int|float)\d*)\b/, 55 | S=/\S/,T=v({keywords:[R,M,L,K,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",N,O,J],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),V={};n(T,["default-code"]);n(E([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-", 56 | /^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),"default-markup htm html mxml xhtml xml xsl".split(" "));n(E([["pln",/^[\s]+/,null," \t\r\n"],["atv",/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/], 57 | ["pun",/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);n(E([],[["atv",/^[\s\S]+/]]),["uq.val"]);n(v({keywords:R,hashComments:!0,cStyleComments:!0,types:P}),"c cc cpp cxx cyc m".split(" "));n(v({keywords:"null,true,false"}),["json"]);n(v({keywords:M,hashComments:!0,cStyleComments:!0, 58 | verbatimStrings:!0,types:P}),["cs"]);n(v({keywords:L,cStyleComments:!0}),["java"]);n(v({keywords:J,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);n(v({keywords:N,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);n(v({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:2}), 59 | ["perl","pl","pm"]);n(v({keywords:O,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);n(v({keywords:K,cStyleComments:!0,regexLiterals:!0}),["javascript","js","ts","typescript"]);n(v({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);n(E([],[["str",/^[\s\S]+/]]), 60 | ["regex"]);var U=Q.PR={createSimpleLexer:E,registerLangHandler:n,sourceDecorator:v,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:function(a,d,f){f=f||!1;d=d||null;var c=document.createElement("div");c.innerHTML="
    "+a+"
    ";c=c.firstChild;f&&B(c,f,!0);H({j:d,m:f,h:c,l:1,a:null,i:null,c:null,g:null}); 61 | return c.innerHTML},prettyPrint:g=function(a,d){function f(){for(var c=Q.PR_SHOULD_USE_CONTINUATION?b.now()+250:Infinity;tPowered by Sauce Labs badges grayvTESTING POWERED BY -------------------------------------------------------------------------------- /st/theme.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica Neue, Helvetica, Arial; 3 | background: rgb(244,215,201); /* Old browsers */ 4 | background: -moz-linear-gradient(top, rgb(244,215,201) 0%, rgb(244,226,201) 100%); /* FF3.6-15 */ 5 | background: -webkit-linear-gradient(top, rgb(244,215,201) 0%,rgb(244,226,201) 100%); /* Chrome10-25,Safari5.1-6 */ 6 | background: linear-gradient(to bottom, rgb(244,215,201) 0%,rgb(244,226,201) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ 7 | margin-bottom: 100px; 8 | } 9 | 10 | .header { 11 | margin-top: 30px; 12 | } 13 | 14 | .header h1 { 15 | margin-top: 10px; 16 | } 17 | 18 | h4 { 19 | padding-bottom: 10px; 20 | } 21 | 22 | .prettyprinted { 23 | margin-top: 5px; 24 | border-top: none !important; 25 | border-bottom: none !important; 26 | border-right: none !important; 27 | border-left: 1px solid rgba(0,0,0,.1) !important; 28 | padding-left: 15px !important; 29 | word-wrap: break-word !important; 30 | overflow: default !important; 31 | text-overflow: default !important; 32 | } 33 | 34 | .tinted { 35 | background-color: #fff6b2; 36 | } 37 | 38 | .handle { 39 | cursor: grab; 40 | } 41 | 42 | code { 43 | color: #606; 44 | } 45 | 46 | .toc { 47 | background-color: rgb(255,255,255,0.5); 48 | border: solid #444 1px; 49 | padding: 20px; 50 | margin-left: auto; 51 | margin-right: auto; 52 | list-style: none; 53 | } 54 | 55 | .toc h5 { 56 | margin-top: 8px; 57 | } 58 | 59 | .list-group-item:hover { 60 | z-index: 0; 61 | } 62 | 63 | .input-section { 64 | background-color: rgb(255,255,255,0.5); 65 | padding: 20px; 66 | } 67 | 68 | .square-section { 69 | background-color: rgb(255,255,255,0.5); 70 | } 71 | 72 | 73 | .square { 74 | width: 20vw; 75 | height: 20vw; 76 | background-color: #00a2ff; 77 | margin-top: 2vw; 78 | margin-left: 2vw; 79 | display: inline-block; 80 | position: relative; 81 | } 82 | 83 | .swap-threshold-indicator { 84 | background-color: #0079bf; 85 | height: 100%; 86 | display: inline-block; 87 | } 88 | 89 | .inverted-swap-threshold-indicator { 90 | background-color: #0079bf; 91 | height: 100%; 92 | position: absolute; 93 | } 94 | 95 | .indicator-left { 96 | left: 0; 97 | top: 0; 98 | } 99 | 100 | .indicator-right { 101 | right: 0; 102 | bottom: 0; 103 | } 104 | 105 | .num-indicator { 106 | position: absolute; 107 | font-size: 50px; 108 | width: 25px; 109 | top: 50%; 110 | left: 50%; 111 | transform: translate(-50%, -50%); 112 | color: white; 113 | } 114 | 115 | .grid-square { 116 | width: 100px; 117 | height: 100px; 118 | display: inline-block; 119 | background-color: #fff; 120 | border: solid 1px rgb(0,0,0,0.2); 121 | padding: 10px; 122 | margin: 12px; 123 | } 124 | 125 | .nested-sortable, .nested-1, .nested-2, .nested-3 { 126 | margin-top: 5px; 127 | } 128 | 129 | .nested-1 { 130 | background-color: #e6e6e6; 131 | } 132 | 133 | .nested-2 { 134 | background-color: #cccccc; 135 | } 136 | 137 | .nested-3 { 138 | background-color: #b3b3b3; 139 | } 140 | 141 | .frameworks { 142 | background-color: rgb(255,255,255,0.5); 143 | border: solid rgb(0,0,0,0.3) 1px; 144 | padding: 20px; 145 | } 146 | 147 | .frameworks h3 { 148 | margin-top: 5px; 149 | } 150 | 151 | input[type=range] { 152 | -webkit-appearance: none; 153 | width: 100%; 154 | margin: 3.8px 0; 155 | } 156 | input[type=range]:focus { 157 | outline: none; 158 | } 159 | input[type=range]::-webkit-slider-runnable-track { 160 | width: 100%; 161 | height: 8.4px; 162 | cursor: pointer; 163 | box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; 164 | background: rgba(48, 113, 169, 0); 165 | border-radius: 1.3px; 166 | border: 0.2px solid #010101; 167 | } 168 | input[type=range]::-webkit-slider-thumb { 169 | box-shadow: 0px 0px 0.9px #000000, 0px 0px 0px #0d0d0d; 170 | border: 1.3px solid rgba(0, 0, 0, 0.7); 171 | height: 16px; 172 | width: 16px; 173 | border-radius: 49px; 174 | background: #ffffff; 175 | cursor: pointer; 176 | -webkit-appearance: none; 177 | margin-top: -4px; 178 | } 179 | input[type=range]:focus::-webkit-slider-runnable-track { 180 | background: rgba(54, 126, 189, 0); 181 | } 182 | input[type=range]::-moz-range-track { 183 | width: 100%; 184 | height: 8.4px; 185 | cursor: pointer; 186 | box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; 187 | background: rgba(48, 113, 169, 0); 188 | border-radius: 1.3px; 189 | border: 0.2px solid #010101; 190 | } 191 | input[type=range]::-moz-range-thumb { 192 | box-shadow: 0px 0px 0.9px #000000, 0px 0px 0px #0d0d0d; 193 | border: 1.3px solid rgba(0, 0, 0, 0.7); 194 | height: 16px; 195 | width: 16px; 196 | border-radius: 49px; 197 | background: #ffffff; 198 | cursor: pointer; 199 | } 200 | input[type=range]::-ms-track { 201 | width: 100%; 202 | height: 8.4px; 203 | cursor: pointer; 204 | background: transparent; 205 | border-color: transparent; 206 | color: transparent; 207 | } 208 | input[type=range]::-ms-fill-lower { 209 | background: rgba(42, 100, 149, 0); 210 | border: 0.2px solid #010101; 211 | border-radius: 2.6px; 212 | box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; 213 | } 214 | input[type=range]::-ms-fill-upper { 215 | background: rgba(48, 113, 169, 0); 216 | border: 0.2px solid #010101; 217 | border-radius: 2.6px; 218 | box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; 219 | } 220 | input[type=range]::-ms-thumb { 221 | box-shadow: 0px 0px 0.9px #000000, 0px 0px 0px #0d0d0d; 222 | border: 1.3px solid rgba(0, 0, 0, 0.7); 223 | height: 16px; 224 | width: 16px; 225 | border-radius: 49px; 226 | background: #ffffff; 227 | cursor: pointer; 228 | height: 8.4px; 229 | } 230 | input[type=range]:focus::-ms-fill-lower { 231 | background: rgba(48, 113, 169, 0); 232 | } 233 | input[type=range]:focus::-ms-fill-upper { 234 | background: rgba(54, 126, 189, 0); 235 | } 236 | 237 | .blue-background-class { 238 | background-color: #C8EBFB; 239 | } 240 | 241 | .col { 242 | padding-right: 0; 243 | margin-right: 15px; 244 | } 245 | 246 | .selected { 247 | background-color: #f9c7c8; 248 | border: solid red 1px !important; 249 | z-index: 1 !important; 250 | } 251 | 252 | .highlight { 253 | background-color: #B7F8C7; 254 | } 255 | -------------------------------------------------------------------------------- /tests/Sortable.compat.test.js: -------------------------------------------------------------------------------- 1 | import { Selector } from 'testcafe'; 2 | 3 | 4 | fixture `Simple Sorting` 5 | .page `./single-list.html`; 6 | 7 | let list1 = Selector('#list1'); 8 | 9 | test('Sort down list', async browser => { 10 | const dragStartPosition = list1.child(0); 11 | const dragEl = await dragStartPosition(); 12 | const dragEndPosition = list1.child(2); 13 | const targetStartPosition = list1.child(2); 14 | const target = await targetStartPosition(); 15 | const targetEndPosition = list1.child(1); 16 | 17 | await browser 18 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 19 | .expect(targetStartPosition.innerText).eql(target.innerText) 20 | .dragToElement(dragEl, target) 21 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 22 | .expect(targetEndPosition.innerText).eql(target.innerText); 23 | }); 24 | 25 | test('Sort up list', async browser => { 26 | const dragStartPosition = list1.child(2); 27 | const dragEl = await dragStartPosition(); 28 | const dragEndPosition = list1.child(0); 29 | const targetStartPosition = list1.child(0); 30 | const target = await targetStartPosition(); 31 | const targetEndPosition = list1.child(1); 32 | 33 | await browser 34 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 35 | .expect(targetStartPosition.innerText).eql(target.innerText) 36 | .dragToElement(dragEl, target) 37 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 38 | .expect(targetEndPosition.innerText).eql(target.innerText); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/Sortable.test.js: -------------------------------------------------------------------------------- 1 | import { Selector } from 'testcafe'; 2 | const itemHeight = 54; // px 3 | const leeway = 1; 4 | 5 | 6 | fixture `Simple Sorting` 7 | .page `./single-list.html`; 8 | 9 | let list1 = Selector('#list1'); 10 | 11 | test('Sort down list', async browser => { 12 | const dragStartPosition = list1.child(0); 13 | const dragEl = await dragStartPosition(); 14 | const dragEndPosition = list1.child(2); 15 | const targetStartPosition = list1.child(2); 16 | const target = await targetStartPosition(); 17 | const targetEndPosition = list1.child(1); 18 | 19 | await browser 20 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 21 | .expect(targetStartPosition.innerText).eql(target.innerText) 22 | .dragToElement(dragEl, target) 23 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 24 | .expect(targetEndPosition.innerText).eql(target.innerText); 25 | }); 26 | 27 | test('Sort up list', async browser => { 28 | const dragStartPosition = list1.child(2); 29 | const dragEl = await dragStartPosition(); 30 | const dragEndPosition = list1.child(0); 31 | const targetStartPosition = list1.child(0); 32 | const target = await targetStartPosition(); 33 | const targetEndPosition = list1.child(1); 34 | 35 | await browser 36 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 37 | .expect(targetStartPosition.innerText).eql(target.innerText) 38 | .dragToElement(dragEl, target) 39 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 40 | .expect(targetEndPosition.innerText).eql(target.innerText); 41 | }); 42 | 43 | test('Swap threshold', async browser => { 44 | const dragStartPosition = list1.child(0); 45 | const dragEl = await dragStartPosition(); 46 | const dragEndPosition = list1.child(1); 47 | const targetStartPosition = list1.child(1); 48 | const target = await targetStartPosition(); 49 | const targetEndPosition = list1.child(0); 50 | 51 | await browser.eval(() => { 52 | Sortable.get(document.getElementById('list1')).option('swapThreshold', 0.6); 53 | }); 54 | 55 | 56 | await browser 57 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 58 | .expect(targetStartPosition.innerText).eql(target.innerText) 59 | .dragToElement(dragEl, target, { 60 | destinationOffsetY: Math.round(itemHeight / 2 * 0.4 - leeway) 61 | }) 62 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 63 | .expect(targetStartPosition.innerText).eql(target.innerText) 64 | .dragToElement(dragEl, target, { 65 | destinationOffsetY: Math.round(itemHeight / 2 * 0.4 + leeway) 66 | }) 67 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 68 | .expect(targetEndPosition.innerText).eql(target.innerText); 69 | }); 70 | 71 | test('Invert swap', async browser => { 72 | const dragStartPosition = list1.child(0); 73 | const dragEl = await dragStartPosition(); 74 | const dragEndPosition = list1.child(1); 75 | const targetStartPosition = list1.child(1); 76 | const target = await targetStartPosition(); 77 | const targetEndPosition = list1.child(0); 78 | 79 | await browser.eval(() => { 80 | Sortable.get(document.getElementById('list1')).option('invertSwap', true); 81 | }); 82 | 83 | 84 | await browser 85 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 86 | .expect(targetStartPosition.innerText).eql(target.innerText) 87 | .dragToElement(dragEl, target, { 88 | destinationOffsetY: Math.round(itemHeight / 2 - leeway) 89 | }) 90 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 91 | .expect(targetStartPosition.innerText).eql(target.innerText) 92 | .dragToElement(dragEl, target, { 93 | destinationOffsetY: Math.round(itemHeight / 2 + leeway) 94 | }) 95 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 96 | .expect(targetEndPosition.innerText).eql(target.innerText); 97 | }); 98 | 99 | 100 | test('Inverted swap threshold', async browser => { 101 | const dragStartPosition = list1.child(0); 102 | const dragEl = await dragStartPosition(); 103 | const dragEndPosition = list1.child(1); 104 | const targetStartPosition = list1.child(1); 105 | const target = await targetStartPosition(); 106 | const targetEndPosition = list1.child(0); 107 | 108 | await browser.eval(() => { 109 | Sortable.get(document.getElementById('list1')).option('invertSwap', true); 110 | Sortable.get(document.getElementById('list1')).option('invertedSwapThreshold', 0.5); 111 | }); 112 | 113 | 114 | await browser 115 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 116 | .expect(targetStartPosition.innerText).eql(target.innerText) 117 | .dragToElement(dragEl, target, { 118 | destinationOffsetY: Math.round(itemHeight - (itemHeight / 2 * 0.5) - leeway) 119 | }) 120 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 121 | .expect(targetStartPosition.innerText).eql(target.innerText) 122 | .dragToElement(dragEl, target, { 123 | destinationOffsetY: Math.round(itemHeight - (itemHeight / 2 * 0.5) + leeway) 124 | }) 125 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 126 | .expect(targetEndPosition.innerText).eql(target.innerText); 127 | }); 128 | 129 | 130 | fixture `Grouping` 131 | .page `./dual-list.html`; 132 | 133 | let list2 = Selector('#list2'); 134 | 135 | test('Move to list of the same group', async browser => { 136 | const dragStartPosition = list1.child(0); 137 | const dragEl = await dragStartPosition(); 138 | const dragEndPosition = list2.child(0); 139 | const targetStartPosition = list2.child(0); 140 | const target = await targetStartPosition(); 141 | const targetEndPosition = list2.child(1); 142 | 143 | await browser.eval(() => { 144 | Sortable.get(document.getElementById('list2')).option('group', 'shared'); 145 | }); 146 | 147 | await browser 148 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 149 | .expect(targetStartPosition.innerText).eql(target.innerText) 150 | .dragToElement(dragEl, target, { offsetY: 0, destinationOffsetY: 0 }) 151 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 152 | .expect(targetEndPosition.innerText).eql(target.innerText); 153 | }); 154 | 155 | 156 | test('Do not move to list of different group', async browser => { 157 | const dragStartPosition = list1.child(0); 158 | const dragEl = await dragStartPosition(); 159 | const targetStartPosition = list2.child(0); 160 | const target = await targetStartPosition(); 161 | 162 | await browser.eval(() => { 163 | Sortable.get(document.getElementById('list2')).option('group', null); 164 | }); 165 | 166 | await browser 167 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 168 | .expect(targetStartPosition.innerText).eql(target.innerText) 169 | .dragToElement(dragEl, target, { offsetY: 0, destinationOffsetY: 0 }) 170 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 171 | .expect(targetStartPosition.innerText).eql(target.innerText); 172 | }); 173 | 174 | 175 | test('Move to list with put:true', async browser => { 176 | // Should allow insert, since pull defaults to `true` 177 | const dragStartPosition = list1.child(0); 178 | const dragEl = await dragStartPosition(); 179 | const dragEndPosition = list2.child(0); 180 | const targetStartPosition = list2.child(0); 181 | const target = await targetStartPosition(); 182 | const targetEndPosition = list2.child(1); 183 | 184 | await browser.eval(() => { 185 | Sortable.get(document.getElementById('list2')).option('group', { put: true }); 186 | }); 187 | 188 | await browser 189 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 190 | .expect(targetStartPosition.innerText).eql(target.innerText) 191 | .dragToElement(dragEl, target, { offsetY: 0, destinationOffsetY: 0 }) 192 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 193 | .expect(targetEndPosition.innerText).eql(target.innerText); 194 | }); 195 | 196 | test('Do not move from list with pull:false', async browser => { 197 | // Should not allow insert, since put defaults to `false` 198 | const dragStartPosition = list1.child(0); 199 | const dragEl = await dragStartPosition(); 200 | const targetStartPosition = list2.child(0); 201 | const target = await targetStartPosition(); 202 | 203 | await browser.eval(() => { 204 | Sortable.get(document.getElementById('list1')).option('group', { pull: false }); 205 | }); 206 | 207 | await browser 208 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 209 | .expect(targetStartPosition.innerText).eql(target.innerText) 210 | .dragToElement(dragEl, target, { offsetY: 0, destinationOffsetY: 0 }) 211 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 212 | .expect(targetStartPosition.innerText).eql(target.innerText); 213 | }); 214 | 215 | test('Clone element if pull:"clone"', async browser => { 216 | const dragStartPosition = list1.child(0); 217 | const dragEl = await dragStartPosition(); 218 | const dragEndPosition = list2.child(0); 219 | const targetStartPosition = list2.child(0); 220 | const target = await targetStartPosition(); 221 | const targetEndPosition = list2.child(1); 222 | 223 | await browser.eval(() => { 224 | Sortable.get(document.getElementById('list1')).option('group', { pull: 'clone' }); 225 | Sortable.get(document.getElementById('list2')).option('group', { put: true }); 226 | }); 227 | 228 | await browser 229 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 230 | .expect(targetStartPosition.innerText).eql(target.innerText) 231 | .dragToElement(dragEl, target, { offsetY: 0, destinationOffsetY: 0 }) 232 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) // clone check 233 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 234 | .expect(targetEndPosition.innerText).eql(target.innerText); 235 | }); 236 | 237 | 238 | 239 | fixture `Handles` 240 | .page `./handles.html`; 241 | 242 | test('Do not allow dragging not using handle', async browser => { 243 | const dragStartPosition = list1.child(0); 244 | const dragEl = await dragStartPosition(); 245 | const targetStartPosition = list1.child(1); 246 | const target = await targetStartPosition(); 247 | 248 | await browser 249 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 250 | .expect(targetStartPosition.innerText).eql(target.innerText) 251 | .dragToElement(dragEl, target) 252 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 253 | .expect(targetStartPosition.innerText).eql(target.innerText); 254 | }); 255 | 256 | 257 | test('Allow dragging using handle', async browser => { 258 | const dragStartPosition = list1.child(0); 259 | const dragEl = await dragStartPosition(); 260 | const dragEndPosition = list1.child(1); 261 | const targetStartPosition = list1.child(1); 262 | const target = await targetStartPosition(); 263 | const targetEndPosition = list1.child(0); 264 | 265 | await browser 266 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 267 | .expect(targetStartPosition.innerText).eql(target.innerText) 268 | .dragToElement(await dragStartPosition.child('.handle'), target) 269 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 270 | .expect(targetEndPosition.innerText).eql(target.innerText); 271 | }); 272 | 273 | fixture `Filter` 274 | .page `./filter.html`; 275 | 276 | test('Do not allow dragging of filtered element', async browser => { 277 | const dragStartPosition = list1.child('.filtered'); 278 | const dragEl = await dragStartPosition(); 279 | const targetStartPosition = dragStartPosition.nextSibling(1); 280 | const target = await targetStartPosition(); 281 | 282 | await browser 283 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 284 | .expect(targetStartPosition.innerText).eql(target.innerText) 285 | .dragToElement(dragEl, target) 286 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 287 | .expect(targetStartPosition.innerText).eql(target.innerText); 288 | }); 289 | 290 | 291 | test('Allow dragging of non-filtered element', async browser => { 292 | const dragStartPosition = list1.child(':not(.filtered)'); 293 | const dragEl = await dragStartPosition(); 294 | const dragEndPosition = dragStartPosition.nextSibling(1); 295 | const targetStartPosition = dragStartPosition.nextSibling(1); 296 | const target = await targetStartPosition(); 297 | const targetEndPosition = dragStartPosition.nextSibling(0); 298 | 299 | await browser 300 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 301 | .expect(targetStartPosition.innerText).eql(target.innerText) 302 | .dragToElement(dragEl, target) 303 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 304 | .expect(targetEndPosition.innerText).eql(target.innerText); 305 | }); 306 | 307 | 308 | 309 | fixture `Nested` 310 | .page `./nested.html`; 311 | 312 | let list1n1 = Selector('.n1'); 313 | let list1n2 = Selector('.n2'); 314 | let list2n1 = Selector('.n1:nth-of-type(2)'); 315 | 316 | test('Dragging from level 1 to level 0', async browser => { 317 | const dragStartPosition = list1n1.child(0); 318 | const dragEl = await dragStartPosition(); 319 | const dragEndPosition = list1.child(2); 320 | const targetStartPosition = list1.child(2); 321 | const target = await targetStartPosition(); 322 | const targetEndPosition = list1.child(3); 323 | 324 | await browser 325 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 326 | .expect(targetStartPosition.innerText).eql(target.innerText) 327 | .dragToElement(dragEl, target, { destinationOffsetY: 0 }) 328 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 329 | .expect(targetEndPosition.innerText).eql(target.innerText); 330 | }); 331 | 332 | 333 | test('Dragging from level 0 to level 2', async browser => { 334 | const dragStartPosition = list1.child(1); 335 | const dragEl = await dragStartPosition(); 336 | const dragEndPosition = list1n2.child(2); 337 | const targetStartPosition = list1n2.child(2); 338 | const target = await targetStartPosition(); 339 | const targetEndPosition = list1n2.child(3); 340 | 341 | await browser 342 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 343 | .expect(targetStartPosition.innerText).eql(target.innerText) 344 | .dragToElement(dragEl, target, { destinationOffsetY: 0 }) 345 | .expect(dragEndPosition.innerText).eql(dragEl.innerText) 346 | .expect(targetEndPosition.innerText).eql(target.innerText); 347 | }); 348 | 349 | 350 | fixture `Empty Insert` 351 | .page `./empty-list.html`; 352 | 353 | test('Insert into empty list if within emptyInsertThreshold', async browser => { 354 | const threshold = await browser.eval(() => Sortable.get(document.getElementById('list2')).option('emptyInsertThreshold')); 355 | const dragStartPosition = list1.child(0); 356 | const dragEl = await dragStartPosition(); 357 | const dragEndPosition = list2.child(0); 358 | // Must use rects since testcafe won't drag to element that is "not visible" 359 | const dragRect = dragEl.boundingClientRect; 360 | const list2Rect = await list2.boundingClientRect; 361 | 362 | 363 | await browser 364 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 365 | .drag(dragEl, Math.round(list2Rect.left - dragRect.left) - (threshold - 1), -(threshold - 1), { 366 | offsetY: 0, 367 | offsetX: 0 368 | }) 369 | .expect(dragEndPosition.innerText).eql(dragEl.innerText); 370 | }); 371 | 372 | test('Do not insert into empty list if outside emptyInsertThreshold', async browser => { 373 | const threshold = await browser.eval(() => Sortable.get(document.getElementById('list2')).option('emptyInsertThreshold')); 374 | const dragStartPosition = list1.child(0); 375 | const dragEl = await dragStartPosition(); 376 | const dragRect = dragEl.boundingClientRect; 377 | const list2Rect = await list2.boundingClientRect; 378 | 379 | await browser 380 | .expect(dragStartPosition.innerText).eql(dragEl.innerText) 381 | .drag(dragEl, Math.round(list2Rect.left - dragRect.left) - (threshold + 1), -(threshold + 1), { 382 | offsetY: 0, 383 | offsetX: 0 384 | }) 385 | .expect(dragStartPosition.innerText).eql(dragEl.innerText); 386 | }); 387 | -------------------------------------------------------------------------------- /tests/dual-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    Item 1.1
    11 |
    Item 1.2
    12 |
    Item 1.3
    13 |
    Item 1.4
    14 |
    Item 1.5
    15 |
    16 | 17 |
    18 |
    Item 2.1
    19 |
    Item 2.2
    20 |
    Item 2.3
    21 |
    Item 2.4
    22 |
    Item 2.5
    23 |
    24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/empty-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    Item 1.1
    11 |
    Item 1.2
    12 |
    Item 1.3
    13 |
    Item 1.4
    14 |
    Item 1.5
    15 |
    16 | 17 |
    18 | 19 |
    20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/filter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    Item 1.1
    11 |
    Item 1.2
    12 |
    Item 1.3
    13 |
    Item 1.4
    14 |
    Item 1.5
    15 |
    16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/handles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    ::Item 1.1
    11 |
    ::Item 1.2
    12 |
    ::Item 1.3
    13 |
    ::Item 1.4
    14 |
    ::Item 1.5
    15 |
    16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/nested.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    Item 1.1 11 |
    12 |
    Item 2.1
    13 |
    Item 2.2 14 |
    15 |
    Item 3.1
    16 |
    Item 3.2
    17 |
    Item 3.3
    18 |
    Item 3.4
    19 |
    20 |
    21 |
    Item 2.3
    22 |
    Item 2.4
    23 |
    24 |
    25 |
    Item 1.2
    26 |
    Item 1.3
    27 |
    Item 1.4 28 |
    29 |
    Item 2.1
    30 |
    Item 2.2
    31 |
    Item 2.3
    32 |
    Item 2.4
    33 |
    34 |
    35 |
    Item 1.5
    36 |
    37 | 38 | 52 | 53 | 54 | 55 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/single-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    Item 1.1
    11 |
    Item 1.2
    12 |
    Item 1.3
    13 |
    Item 1.4
    14 |
    Item 1.5
    15 |
    16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/style.css: -------------------------------------------------------------------------------- 1 | .list > div { 2 | min-height: 50px; 3 | border-style: solid; 4 | border-width: 2px; 5 | text-align: center; 6 | line-height: 50px; 7 | font-size: 20px; 8 | font-family: Helvetica; 9 | } 10 | 11 | 12 | .half { 13 | display: inline-block; 14 | width: 49%; 15 | padding: 0; 16 | margin: 0; 17 | vertical-align: top; 18 | } 19 | --------------------------------------------------------------------------------