├── .editorconfig ├── .github ├── FUNDING.yml ├── contributing.markdown └── issue_template.markdown ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── changelog.markdown ├── classes.js ├── dist ├── dragula.css ├── dragula.js ├── dragula.min.css └── dragula.min.js ├── dragula.js ├── dragula.styl ├── example ├── example.css ├── example.js └── example.min.js ├── favicon.ico ├── index.html ├── license ├── package.json ├── readme.markdown ├── resources ├── demo.png ├── eyes.png ├── icon.png ├── icon.svg ├── logo.png ├── logo.svg └── patreon.svg ├── test ├── cancel.js ├── classes.js ├── containers.js ├── defaults.js ├── destroy.js ├── drag.js ├── drake-api.js ├── end.js ├── events.js ├── lib │ └── events.js ├── public-api.js └── remove.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: bevacqua 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/contributing.markdown: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Hey there! Glad you want to chime in. Here's what you need to know. 4 | 5 | ### Support Requests 6 | 7 | We have a [dedicated support channel in Slack][4]. See [this issue][3] to get an invite. Support requests won't be handled through the repository. 8 | 9 | If you have a question, make sure it wasn't [already answered][1]. If it wasn't, please refer to the Slack chat. To get an invite, use the badge in the [demo page][2]. 10 | 11 | > Our goal is to provide answers to the most frequently asked questions somewhere in the documentation. 12 | 13 | ### Bugs 14 | 15 | Bug reports are tricky. Please provide as much context as possible, and if you want to start working on a fix, we'll be forever grateful! Please try and test around for a bit to make sure you're dealing with a bug and not an issue in your implementation. 16 | 17 | If possible, provide a demo where the bug is isolated and turned into its smallest possible representation. That would help a lot! 18 | 19 | Thanks for reporting bugs, we'd be lost without you. 20 | 21 | ### Feature Requests 22 | 23 | We're still considering feature requests. Note that we might not implement the feature you want, or exactly how you want it. The goal here is to keep making `dragula` awesome while not making it too bloated. 24 | 25 | We also dislike overly specific features and favor more abstract ones you the consumer can build other features upon. 26 | 27 | # Development 28 | 29 | Development flows are based on `npm run` scripts. 30 | 31 | ### Build 32 | 33 | To compile a standalone browserify module, use the following command. A minified version will also be produced. The compiled bundles are placed inside `dist`. Since **these are autogenerated**, please don't include them in your pull requests. 34 | 35 | ```shell 36 | npm run build 37 | ``` 38 | 39 | You can also run the build continuously, _to facilitate development_, with this command. 40 | 41 | ```shell 42 | npm start 43 | ``` 44 | 45 | ### Test 46 | 47 | Run the command below to execute all tests in a DevTools window through Electron. Note that the DevTools will get reloaded whenever your test files change, making tests a breeze! 48 | 49 | ```shell 50 | npm run test-watch 51 | ``` 52 | 53 | To run tests a single time, simply run the following command. This is used in CI testing. 54 | 55 | ```shell 56 | npm test 57 | ``` 58 | 59 | [1]: https://github.com/bevacqua/dragula/issues?q=label%3Asupport 60 | [2]: http://bevacqua.github.io/dragula/ 61 | [3]: https://github.com/bevacqua/dragula/issues/248 62 | [4]: https://dragula.slack.com 63 | -------------------------------------------------------------------------------- /.github/issue_template.markdown: -------------------------------------------------------------------------------- 1 | > Please only use GitHub issues for bug reports and feature requests. 2 | > 3 | > - [ ] Read the [contributing guidelines][contrib] 4 | > - [ ] Bug reports containing repro steps are likely to be fixed faster 5 | > - [ ] Feature requests should be multi-purpose, describe use cases 6 | > - [ ] For support requests or questions, please refer to [our Slack channel][slack] 7 | 8 | [contrib]: https://github.com/bevacqua/dragula/blob/master/.github/contributing.markdown 9 | [slack]: https://dragula.slack.com 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | Thumbs.db 5 | package-lock.json 6 | .idea/* 7 | test/test.html 8 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | example 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "newcap": true, 5 | "noarg": true, 6 | "noempty": true, 7 | "nonew": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "trailing": true, 12 | "boss": true, 13 | "eqnull": true, 14 | "strict": true, 15 | "immed": true, 16 | "expr": true, 17 | "latedef": "nofunc", 18 | "quotmark": "single", 19 | "validthis": true, 20 | "indent": 2, 21 | "node": true, 22 | "browser": true 23 | } 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'lts/*' 4 | addons: 5 | apt: 6 | packages: 7 | - xvfb 8 | install: 9 | - export DISPLAY=':99.0' 10 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 11 | - npm install 12 | -------------------------------------------------------------------------------- /changelog.markdown: -------------------------------------------------------------------------------- 1 | # 3.7.3 Up with the times 2 | 3 | - Added ability to set pixel delay in X and Y drag coordinates 4 | - Bumped `crossvent@1.5.5` 5 | - Updated all dev dependencies and fixed testing 6 | - Removed Bower Support 7 | 8 | # 3.7.2 Parental Control 9 | 10 | - Fixed a bug where a missing parent check would cause exceptions 11 | 12 | # 3.7.1 Contrarian Views 13 | 14 | - Bumped `contra@1.9.4` 15 | 16 | # 3.7.0 Party Like It's the End of the World 17 | 18 | - Added a `canMove` method that returns whether `drake` can move a DOM element 19 | 20 | # 3.6.8 Calculated Risk 21 | 22 | - Fixed a bug where `drake.cancel` would misbehave when `copy` was `true` 23 | 24 | # 3.6.7 Miscalculation 25 | 26 | - Fixed a long-standing bug where candidate positioning would be off by one position 27 | 28 | # 3.6.6 Living on the Edge 29 | 30 | - Fixed a bug with clicks on IE7, IE8, IE9 31 | 32 | # 3.6.5 Shadowbane 33 | 34 | - Fixed a bug where `shadow` even would trigger multiple times when moving over the last position 35 | 36 | # 3.6.4 Water Tap 37 | 38 | - Fixed an instrumentation issue that prevented the tests from running 39 | 40 | # 3.6.3 Body Double 41 | 42 | - Fixed an issue that prevented `dragula` from execution early in the document load life-cycle 43 | 44 | # 3.6.2 Rugrats 45 | 46 | - Fixed a _touch events_ regression introduced in `3.6.1` 47 | 48 | # 3.6.1 Point Blank 49 | 50 | - Fixed issues in touch-enabled browsers such as Windows Phone 10 51 | 52 | # 3.6.0 Prognosis Negative 53 | 54 | - Introduced support for `contentEditable` DOM attribute 55 | 56 | # 3.5.4 Parental Discretion 57 | 58 | - Switched from `.parentElement` to `.parentNode` avoiding bugs when hovering over `` elements 59 | 60 | # 3.5.3 Dragster 61 | 62 | - Fixed a bug where mobile devices wouldn't be able to drag elements 63 | 64 | # 3.5.2 Press Start 65 | 66 | - Fixed a bug where `` 222 | 223 | # 1.4.1 Blood Prince 224 | 225 | - Fixed an issue where manually started drag events wouldn't know if position changed when an item was dropped in the source container 226 | - Added minor styling to `gu-mirror`, to visually identify that a drag is in progress 227 | 228 | # 1.4.0 Top Fuel 229 | 230 | - Added a `dragend` event that's always fired 231 | - Added a `dragging` property to API 232 | - Introduced manual `start` API method 233 | - Introduced `addContainer` and `removeContainer` dynamic API 234 | 235 | # 1.3.0 Terror 236 | 237 | Introduced an `.end` instance API method that gracefully ends the drag event using the last known valid drop target. 238 | 239 | # 1.2.4 Brother in Arms 240 | 241 | - The `accepts` option now takes a fourth argument, `sibling`, giving us a hint of the precise position the item would be dropped in 242 | 243 | # 1.2.3 Breeding Pool 244 | 245 | - Fixed a bug in cross browser behavior that caused the hover effect to ignore scrolling 246 | - Fixed a bug where touch events weren't working in obscure versions of IE 247 | 248 | # 1.2.2 Originality Accepted 249 | 250 | - Improved `accepts` mechanism so that it always accepts the original starting point 251 | 252 | # 1.2.1 Firehose 253 | 254 | - Fixed a bug introduced in `1.2.0` 255 | - Fixed a bug where cancelling with `revert` enabled wouldn't respect sort order 256 | 257 | # 1.2.0 Firefly 258 | 259 | - Introduced `moves` option, used to determine if an item is draggable 260 | - Added a `source` parameter for the `drop` event 261 | - Cancelling a drag event when `revertOnSpill` is `true` will now move the element to its original position in the source element instead of appending it 262 | - Fixed a bug where _"cancellations"_ that ended up leaving the dragged element in the source container but changed sort order would trigger a `cancel` event instead of `drop` 263 | - Fixed a bug where _"drops"_ that ended up leaving the element in the exact same place it was dragged from would end up triggering a `drop` event instead of `cancel` 264 | - Added touch event support 265 | 266 | # 1.1.4 Fog Creek 267 | 268 | - Added `'shadow'` event to enable easy updates to shadow element as it's moved 269 | 270 | # 1.1.3 Drag Queen 271 | 272 | - Fixed a bug where `dragula` wouldn't make a copy if the element was dropped outside of a target container 273 | - If a dragged element gets removed for an instance that has `copy` set to `true`, a `cancel` event is raised instead 274 | 275 | # 1.1.2 Eavesdropping 276 | 277 | - Fixed a bug where _"cancellations"_ that ended up leaving the dragged element somewhere other than the source container wouldn't trigger a `drop` event 278 | 279 | # 1.1.1 Slipping Jimmy 280 | 281 | - Fixed a bug where the movable shadow wouldn't update properly if the element was hovered over the last position of a container 282 | 283 | # 1.1.0 Age of Shadows 284 | 285 | - Added a movable shadow that gives visual feedback as to where a dragged item would be dropped 286 | - Added an option to remove dragged elements when they are dropped outside of sanctioned containers 287 | - Added an option to revert dragged elements back to their source container when they are dropped outside of sanctioned containers 288 | 289 | # 1.0.1 Consuelo 290 | 291 | - Removed `console.log` statement 292 | 293 | # 1.0.0 IPO 294 | 295 | - Initial Public Release 296 | -------------------------------------------------------------------------------- /classes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var cache = {}; 4 | var start = '(?:^|\\s)'; 5 | var end = '(?:\\s|$)'; 6 | 7 | function lookupClass (className) { 8 | var cached = cache[className]; 9 | if (cached) { 10 | cached.lastIndex = 0; 11 | } else { 12 | cache[className] = cached = new RegExp(start + className + end, 'g'); 13 | } 14 | return cached; 15 | } 16 | 17 | function addClass (el, className) { 18 | var current = el.className; 19 | if (!current.length) { 20 | el.className = className; 21 | } else if (!lookupClass(className).test(current)) { 22 | el.className += ' ' + className; 23 | } 24 | } 25 | 26 | function rmClass (el, className) { 27 | el.className = el.className.replace(lookupClass(className), ' ').trim(); 28 | } 29 | 30 | module.exports = { 31 | add: addClass, 32 | rm: rmClass 33 | }; 34 | -------------------------------------------------------------------------------- /dist/dragula.css: -------------------------------------------------------------------------------- 1 | .gu-mirror { 2 | position: fixed !important; 3 | margin: 0 !important; 4 | z-index: 9999 !important; 5 | opacity: 0.8; 6 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; 7 | filter: alpha(opacity=80); 8 | } 9 | .gu-hide { 10 | display: none !important; 11 | } 12 | .gu-unselectable { 13 | -webkit-user-select: none !important; 14 | -moz-user-select: none !important; 15 | -ms-user-select: none !important; 16 | user-select: none !important; 17 | } 18 | .gu-transit { 19 | opacity: 0.2; 20 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)"; 21 | filter: alpha(opacity=20); 22 | } 23 | -------------------------------------------------------------------------------- /dist/dragula.min.css: -------------------------------------------------------------------------------- 1 | .gu-mirror{position:fixed!important;margin:0!important;z-index:9999!important;opacity:.8}.gu-hide{display:none!important}.gu-unselectable{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.gu-transit{opacity:.2} -------------------------------------------------------------------------------- /dist/dragula.min.js: -------------------------------------------------------------------------------- 1 | !function(e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).dragula=e()}(function(){return function o(r,i,u){function c(t,e){if(!i[t]){if(!r[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(a)return a(t,!0);throw(n=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",n}n=i[t]={exports:{}},r[t][0].call(n.exports,function(e){return c(r[t][1][e]||e)},n,n.exports,o,r,i,u)}return i[t].exports}for(var a="function"==typeof require&&require,e=0;ee.left+G(e)/2);return n(u>e.top+J(e)/2)}:function(){var e,t,n,o=r.children.length;for(e=0;ei)return t;if(!c&&n.top+n.height/2>u)return t}return null})();function n(e){return e?Z(t):t}}}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./classes":1,"contra/emitter":5,crossvent:6}],3:[function(e,t,n){t.exports=function(e,t){return Array.prototype.slice.call(e,t)}},{}],4:[function(e,t,n){"use strict";var o=e("ticky");t.exports=function(e,t,n){e&&o(function(){e.apply(n||null,t||[])})}},{ticky:10}],5:[function(e,t,n){"use strict";var c=e("atoa"),a=e("./debounce");t.exports=function(r,e){var i=e||{},u={};return void 0===r&&(r={}),r.on=function(e,t){return u[e]?u[e].push(t):u[e]=[t],r},r.once=function(e,t){return t._once=!0,r.on(e,t),r},r.off=function(e,t){var n=arguments.length;if(1===n)delete u[e];else if(0===n)u={};else{e=u[e];if(!e)return r;e.splice(e.indexOf(t),1)}return r},r.emit=function(){var e=c(arguments);return r.emitterSnapshot(e.shift()).apply(this,e)},r.emitterSnapshot=function(o){var e=(u[o]||[]).slice(0);return function(){var t=c(arguments),n=this||r;if("error"===o&&!1!==i.throws&&!e.length)throw 1===t.length?t[0]:t;return e.forEach(function(e){i.async?a(e,t,n):e.apply(n,t),e._once&&r.off(o,e)}),r}},r}},{"./debounce":4,atoa:3}],6:[function(n,o,e){(function(r){"use strict";var i=n("custom-event"),u=n("./eventmap"),c=r.document,e=function(e,t,n,o){return e.addEventListener(t,n,o)},t=function(e,t,n,o){return e.removeEventListener(t,n,o)},a=[];function l(e,t,n){t=function(e,t,n){var o,r;for(o=0;o 0 ? revert : o.revertOnSpill; 287 | var item = _copy || _item; 288 | var parent = getParent(item); 289 | var initial = isInitialPlacement(parent); 290 | if (initial === false && reverts) { 291 | if (_copy) { 292 | if (parent) { 293 | parent.removeChild(_copy); 294 | } 295 | } else { 296 | _source.insertBefore(item, _initialSibling); 297 | } 298 | } 299 | if (initial || reverts) { 300 | drake.emit('cancel', item, _source, _source); 301 | } else { 302 | drake.emit('drop', item, parent, _source, _currentSibling); 303 | } 304 | cleanup(); 305 | } 306 | 307 | function cleanup () { 308 | var item = _copy || _item; 309 | ungrab(); 310 | removeMirrorImage(); 311 | if (item) { 312 | classes.rm(item, 'gu-transit'); 313 | } 314 | if (_renderTimer) { 315 | clearTimeout(_renderTimer); 316 | } 317 | drake.dragging = false; 318 | if (_lastDropTarget) { 319 | drake.emit('out', item, _lastDropTarget, _source); 320 | } 321 | drake.emit('dragend', item); 322 | _source = _item = _copy = _initialSibling = _currentSibling = _renderTimer = _lastDropTarget = null; 323 | } 324 | 325 | function isInitialPlacement (target, s) { 326 | var sibling; 327 | if (s !== void 0) { 328 | sibling = s; 329 | } else if (_mirror) { 330 | sibling = _currentSibling; 331 | } else { 332 | sibling = nextEl(_copy || _item); 333 | } 334 | return target === _source && sibling === _initialSibling; 335 | } 336 | 337 | function findDropTarget (elementBehindCursor, clientX, clientY) { 338 | var target = elementBehindCursor; 339 | while (target && !accepted()) { 340 | target = getParent(target); 341 | } 342 | return target; 343 | 344 | function accepted () { 345 | var droppable = isContainer(target); 346 | if (droppable === false) { 347 | return false; 348 | } 349 | 350 | var immediate = getImmediateChild(target, elementBehindCursor); 351 | var reference = getReference(target, immediate, clientX, clientY); 352 | var initial = isInitialPlacement(target, reference); 353 | if (initial) { 354 | return true; // should always be able to drop it right back where it was 355 | } 356 | return o.accepts(_item, target, _source, reference); 357 | } 358 | } 359 | 360 | function drag (e) { 361 | if (!_mirror) { 362 | return; 363 | } 364 | e.preventDefault(); 365 | 366 | var clientX = getCoord('clientX', e) || 0; 367 | var clientY = getCoord('clientY', e) || 0; 368 | var x = clientX - _offsetX; 369 | var y = clientY - _offsetY; 370 | 371 | _mirror.style.left = x + 'px'; 372 | _mirror.style.top = y + 'px'; 373 | 374 | var item = _copy || _item; 375 | var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY); 376 | var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY); 377 | var changed = dropTarget !== null && dropTarget !== _lastDropTarget; 378 | if (changed || dropTarget === null) { 379 | out(); 380 | _lastDropTarget = dropTarget; 381 | over(); 382 | } 383 | var parent = getParent(item); 384 | if (dropTarget === _source && _copy && !o.copySortSource) { 385 | if (parent) { 386 | parent.removeChild(item); 387 | } 388 | return; 389 | } 390 | var reference; 391 | var immediate = getImmediateChild(dropTarget, elementBehindCursor); 392 | if (immediate !== null) { 393 | reference = getReference(dropTarget, immediate, clientX, clientY); 394 | } else if (o.revertOnSpill === true && !_copy) { 395 | reference = _initialSibling; 396 | dropTarget = _source; 397 | } else { 398 | if (_copy && parent) { 399 | parent.removeChild(item); 400 | } 401 | return; 402 | } 403 | if ( 404 | (reference === null && changed) || 405 | reference !== item && 406 | reference !== nextEl(item) 407 | ) { 408 | _currentSibling = reference; 409 | dropTarget.insertBefore(item, reference); 410 | drake.emit('shadow', item, dropTarget, _source); 411 | } 412 | function moved (type) { drake.emit(type, item, _lastDropTarget, _source); } 413 | function over () { if (changed) { moved('over'); } } 414 | function out () { if (_lastDropTarget) { moved('out'); } } 415 | } 416 | 417 | function spillOver (el) { 418 | classes.rm(el, 'gu-hide'); 419 | } 420 | 421 | function spillOut (el) { 422 | if (drake.dragging) { classes.add(el, 'gu-hide'); } 423 | } 424 | 425 | function renderMirrorImage () { 426 | if (_mirror) { 427 | return; 428 | } 429 | var rect = _item.getBoundingClientRect(); 430 | _mirror = _item.cloneNode(true); 431 | _mirror.style.width = getRectWidth(rect) + 'px'; 432 | _mirror.style.height = getRectHeight(rect) + 'px'; 433 | classes.rm(_mirror, 'gu-transit'); 434 | classes.add(_mirror, 'gu-mirror'); 435 | o.mirrorContainer.appendChild(_mirror); 436 | touchy(documentElement, 'add', 'mousemove', drag); 437 | classes.add(o.mirrorContainer, 'gu-unselectable'); 438 | drake.emit('cloned', _mirror, _item, 'mirror'); 439 | } 440 | 441 | function removeMirrorImage () { 442 | if (_mirror) { 443 | classes.rm(o.mirrorContainer, 'gu-unselectable'); 444 | touchy(documentElement, 'remove', 'mousemove', drag); 445 | getParent(_mirror).removeChild(_mirror); 446 | _mirror = null; 447 | } 448 | } 449 | 450 | function getImmediateChild (dropTarget, target) { 451 | var immediate = target; 452 | while (immediate !== dropTarget && getParent(immediate) !== dropTarget) { 453 | immediate = getParent(immediate); 454 | } 455 | if (immediate === documentElement) { 456 | return null; 457 | } 458 | return immediate; 459 | } 460 | 461 | function getReference (dropTarget, target, x, y) { 462 | var horizontal = o.direction === 'horizontal'; 463 | var reference = target !== dropTarget ? inside() : outside(); 464 | return reference; 465 | 466 | function outside () { // slower, but able to figure out any position 467 | var len = dropTarget.children.length; 468 | var i; 469 | var el; 470 | var rect; 471 | for (i = 0; i < len; i++) { 472 | el = dropTarget.children[i]; 473 | rect = el.getBoundingClientRect(); 474 | if (horizontal && (rect.left + rect.width / 2) > x) { return el; } 475 | if (!horizontal && (rect.top + rect.height / 2) > y) { return el; } 476 | } 477 | return null; 478 | } 479 | 480 | function inside () { // faster, but only available if dropped inside a child element 481 | var rect = target.getBoundingClientRect(); 482 | if (horizontal) { 483 | return resolve(x > rect.left + getRectWidth(rect) / 2); 484 | } 485 | return resolve(y > rect.top + getRectHeight(rect) / 2); 486 | } 487 | 488 | function resolve (after) { 489 | return after ? nextEl(target) : target; 490 | } 491 | } 492 | 493 | function isCopy (item, container) { 494 | return typeof o.copy === 'boolean' ? o.copy : o.copy(item, container); 495 | } 496 | } 497 | 498 | function touchy (el, op, type, fn) { 499 | var touch = { 500 | mouseup: 'touchend', 501 | mousedown: 'touchstart', 502 | mousemove: 'touchmove' 503 | }; 504 | var pointers = { 505 | mouseup: 'pointerup', 506 | mousedown: 'pointerdown', 507 | mousemove: 'pointermove' 508 | }; 509 | var microsoft = { 510 | mouseup: 'MSPointerUp', 511 | mousedown: 'MSPointerDown', 512 | mousemove: 'MSPointerMove' 513 | }; 514 | if (global.navigator.pointerEnabled) { 515 | crossvent[op](el, pointers[type], fn); 516 | } else if (global.navigator.msPointerEnabled) { 517 | crossvent[op](el, microsoft[type], fn); 518 | } else { 519 | crossvent[op](el, touch[type], fn); 520 | crossvent[op](el, type, fn); 521 | } 522 | } 523 | 524 | function whichMouseButton (e) { 525 | if (e.touches !== void 0) { return e.touches.length; } 526 | if (e.which !== void 0 && e.which !== 0) { return e.which; } // see https://github.com/bevacqua/dragula/issues/261 527 | if (e.buttons !== void 0) { return e.buttons; } 528 | var button = e.button; 529 | if (button !== void 0) { // see https://github.com/jquery/jquery/blob/99e8ff1baa7ae341e94bb89c3e84570c7c3ad9ea/src/event.js#L573-L575 530 | return button & 1 ? 1 : button & 2 ? 3 : (button & 4 ? 2 : 0); 531 | } 532 | } 533 | 534 | function getOffset (el) { 535 | var rect = el.getBoundingClientRect(); 536 | return { 537 | left: rect.left + getScroll('scrollLeft', 'pageXOffset'), 538 | top: rect.top + getScroll('scrollTop', 'pageYOffset') 539 | }; 540 | } 541 | 542 | function getScroll (scrollProp, offsetProp) { 543 | if (typeof global[offsetProp] !== 'undefined') { 544 | return global[offsetProp]; 545 | } 546 | if (documentElement.clientHeight) { 547 | return documentElement[scrollProp]; 548 | } 549 | return doc.body[scrollProp]; 550 | } 551 | 552 | function getElementBehindPoint (point, x, y) { 553 | point = point || {}; 554 | var state = point.className || ''; 555 | var el; 556 | point.className += ' gu-hide'; 557 | el = doc.elementFromPoint(x, y); 558 | point.className = state; 559 | return el; 560 | } 561 | 562 | function never () { return false; } 563 | function always () { return true; } 564 | function getRectWidth (rect) { return rect.width || (rect.right - rect.left); } 565 | function getRectHeight (rect) { return rect.height || (rect.bottom - rect.top); } 566 | function getParent (el) { return el.parentNode === doc ? null : el.parentNode; } 567 | function isInput (el) { return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' || isEditable(el); } 568 | function isEditable (el) { 569 | if (!el) { return false; } // no parents were editable 570 | if (el.contentEditable === 'false') { return false; } // stop the lookup 571 | if (el.contentEditable === 'true') { return true; } // found a contentEditable element in the chain 572 | return isEditable(getParent(el)); // contentEditable is set to 'inherit' 573 | } 574 | 575 | function nextEl (el) { 576 | return el.nextElementSibling || manually(); 577 | function manually () { 578 | var sibling = el; 579 | do { 580 | sibling = sibling.nextSibling; 581 | } while (sibling && sibling.nodeType !== 1); 582 | return sibling; 583 | } 584 | } 585 | 586 | function getEventHost (e) { 587 | // on touchend event, we have to use `e.changedTouches` 588 | // see http://stackoverflow.com/questions/7192563/touchend-event-properties 589 | // see https://github.com/bevacqua/dragula/issues/34 590 | if (e.targetTouches && e.targetTouches.length) { 591 | return e.targetTouches[0]; 592 | } 593 | if (e.changedTouches && e.changedTouches.length) { 594 | return e.changedTouches[0]; 595 | } 596 | return e; 597 | } 598 | 599 | function getCoord (coord, e) { 600 | var host = getEventHost(e); 601 | var missMap = { 602 | pageX: 'clientX', // IE8 603 | pageY: 'clientY' // IE8 604 | }; 605 | if (coord in missMap && !(coord in host) && missMap[coord] in host) { 606 | coord = missMap[coord]; 607 | } 608 | return host[coord]; 609 | } 610 | 611 | module.exports = dragula; 612 | -------------------------------------------------------------------------------- /dragula.styl: -------------------------------------------------------------------------------- 1 | .gu-mirror 2 | position fixed !important 3 | margin 0 !important 4 | z-index 9999 !important 5 | opacity 0.8 6 | 7 | .gu-hide 8 | display none !important 9 | 10 | .gu-unselectable 11 | user-select none !important 12 | 13 | .gu-transit 14 | opacity 0.2 15 | -------------------------------------------------------------------------------- /example/example.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #942A57; 3 | margin: 0 auto; 4 | max-width: 760px; 5 | } 6 | 7 | html, body { 8 | -webkit-box-sizing: border-box; 9 | -moz-box-sizing: border-box; 10 | box-sizing: border-box; 11 | } 12 | 13 | *, *:before, *:after { 14 | -webkit-box-sizing: inherit; 15 | -moz-box-sizing: inherit; 16 | box-sizing: inherit; 17 | } 18 | 19 | body, input, button { 20 | font-family: Georgia, Helvetica; 21 | font-size: 17px; 22 | color: #ecf0f1; 23 | } 24 | 25 | h1 { 26 | text-align: center; 27 | background-color: #AC5C7E; 28 | margin-top: 20px; 29 | margin-bottom: 0; 30 | padding: 10px; 31 | } 32 | 33 | h3 { 34 | background-color: rgba(255, 255, 255, 0.2); 35 | border-bottom: 5px solid #A13462; 36 | text-align: center; 37 | padding: 10px; 38 | } 39 | 40 | h3 div { 41 | margin-bottom: 10px; 42 | } 43 | 44 | .tagline { 45 | position: relative; 46 | margin-top: 0; 47 | } 48 | .tagline-text { 49 | vertical-align: middle; 50 | } 51 | 52 | .promo { 53 | margin-bottom: 0; 54 | font-style: italic; 55 | padding: 10px; 56 | background-color: #ff4020; 57 | border-bottom: 5px solid #c00; 58 | } 59 | 60 | a { 61 | font-weight: bold; 62 | } 63 | a, 64 | a:hover { 65 | color: #ecf0f1; 66 | } 67 | 68 | pre { 69 | white-space: pre-wrap; 70 | } 71 | 72 | pre code { 73 | color: #fff; 74 | font-size: 14px; 75 | line-height: 1.3; 76 | } 77 | 78 | label { 79 | display: block; 80 | margin-bottom: 15px; 81 | } 82 | 83 | #superSecret { 84 | opacity: 0.01; 85 | background-color: rgb(0,0,0,0.3); 86 | height: 60px; 87 | width: 100%; 88 | transition: 0.2s; 89 | object-fit: cover; 90 | } 91 | 92 | #superSecret:hover { 93 | animation: superSUPERSecret; 94 | animation-duration: 0.5s; 95 | animation-iteration-count: infinite; 96 | opacity: 0.8; 97 | background-color: rgb(0,0,0,0.1); 98 | } 99 | 100 | @keyframes superSUPERSecret { 101 | 0%{transform: rotate(3deg)} 102 | 50%{transform: rotate(-3deg)} 103 | 100%{transform: rotate(3deg)} 104 | } 105 | 106 | sub { 107 | display: block; 108 | text-align: right; 109 | margin-top: -10px; 110 | font-size: 11px; 111 | font-style: italic; 112 | } 113 | 114 | ul { 115 | margin: 0; 116 | padding: 0; 117 | } 118 | 119 | .parent { 120 | background-color: rgba(255, 255, 255, 0.2); 121 | margin: 50px 0; 122 | padding: 20px; 123 | } 124 | 125 | input { 126 | border: none; 127 | outline: none; 128 | background-color: #ecf0f1; 129 | padding: 10px; 130 | color: #942A57; 131 | border: 0; 132 | margin: 5px 0; 133 | display: block; 134 | width: 100%; 135 | } 136 | 137 | button { 138 | background-color: #ecf0f1; 139 | color: #942A57; 140 | border: 0; 141 | padding: 18px 12px; 142 | margin-left: 6px; 143 | cursor: pointer; 144 | outline: none; 145 | } 146 | 147 | button:hover { 148 | background-color: #e74c3c; 149 | color: #ecf0f1; 150 | } 151 | 152 | .gh-fork { 153 | position: fixed; 154 | top: 0; 155 | right: 0; 156 | border: 0; 157 | } 158 | 159 | /* dragula-specific example page styles */ 160 | .wrapper { 161 | display: table; 162 | } 163 | .container { 164 | display: table-cell; 165 | background-color: rgba(255, 255, 255, 0.2); 166 | width: 50%; 167 | } 168 | .container:nth-child(odd) { 169 | background-color: rgba(0, 0, 0, 0.2); 170 | } 171 | /* 172 | * note that styling gu-mirror directly is a bad practice because it's too generic. 173 | * you're better off giving the draggable elements a unique class and styling that directly! 174 | */ 175 | .container > div, 176 | .gu-mirror { 177 | margin: 10px; 178 | padding: 10px; 179 | background-color: rgba(0, 0, 0, 0.2); 180 | transition: opacity 0.4s ease-in-out; 181 | } 182 | .container > div { 183 | cursor: move; 184 | cursor: grab; 185 | cursor: -moz-grab; 186 | cursor: -webkit-grab; 187 | } 188 | .gu-mirror { 189 | cursor: grabbing; 190 | cursor: -moz-grabbing; 191 | cursor: -webkit-grabbing; 192 | } 193 | .container .ex-moved { 194 | background-color: #e74c3c; 195 | } 196 | .container.ex-over { 197 | background-color: rgba(255, 255, 255, 0.3); 198 | } 199 | #left-lovehandles > div, 200 | #right-lovehandles > div { 201 | cursor: initial; 202 | } 203 | .handle { 204 | padding: 0 5px; 205 | margin-right: 5px; 206 | background-color: rgba(0, 0, 0, 0.4); 207 | cursor: move; 208 | } 209 | .image-thing { 210 | margin: 20px 0; 211 | display: block; 212 | text-align: center; 213 | } 214 | .slack-join { 215 | position: absolute; 216 | font-weight: normal; 217 | font-size: 14px; 218 | right: 10px; 219 | top: 50%; 220 | margin-top: -8px; 221 | line-height: 16px; 222 | } 223 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crossvent = require('crossvent'); 4 | var sortable = $('sortable'); 5 | 6 | dragula([$('left-defaults'), $('right-defaults')]); 7 | dragula([$('left-copy'), $('right-copy')], { copy: true }); 8 | dragula([$('left-events'), $('right-events')]) 9 | .on('drag', function (el) { 10 | el.className = el.className.replace('ex-moved', ''); 11 | }) 12 | .on('drop', function (el) { 13 | el.className += ' ex-moved'; 14 | }) 15 | .on('over', function (el, container) { 16 | container.className += ' ex-over'; 17 | }) 18 | .on('out', function (el, container) { 19 | container.className = container.className.replace('ex-over', ''); 20 | }); 21 | dragula([$('left-rollbacks'), $('right-rollbacks')], { revertOnSpill: true }); 22 | dragula([$('left-lovehandles'), $('right-lovehandles')], { 23 | moves: function (el, container, handle) { 24 | return handle.classList.contains('handle'); 25 | } 26 | }); 27 | 28 | dragula([$('left-rm-spill'), $('right-rm-spill')], { removeOnSpill: true }); 29 | dragula([$('left-copy-1tomany'), $('right-copy-1tomany')], { 30 | copy: function (el, source) { 31 | return source === $('left-copy-1tomany'); 32 | }, 33 | accepts: function (el, target) { 34 | return target !== $('left-copy-1tomany'); 35 | } 36 | }); 37 | 38 | dragula([sortable]); 39 | 40 | crossvent.add(sortable, 'click', clickHandler); 41 | 42 | function clickHandler (e) { 43 | var target = e.target; 44 | if (target === sortable) { 45 | return; 46 | } 47 | target.innerHTML += ' [click!]'; 48 | 49 | setTimeout(function () { 50 | target.innerHTML = target.innerHTML.replace(/ \[click!\]/g, ''); 51 | }, 500); 52 | } 53 | 54 | function $ (id) { 55 | return document.getElementById(id); 56 | } 57 | -------------------------------------------------------------------------------- /example/example.min.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 9 84 | 'function' === typeof document.createEvent ? function CustomEvent (type, params) { 85 | var e = document.createEvent('CustomEvent'); 86 | if (params) { 87 | e.initCustomEvent(type, params.bubbles, params.cancelable, params.detail); 88 | } else { 89 | e.initCustomEvent(type, false, false, void 0); 90 | } 91 | return e; 92 | } : 93 | 94 | // IE <= 8 95 | function CustomEvent (type, params) { 96 | var e = document.createEventObject(); 97 | e.type = type; 98 | if (params) { 99 | e.bubbles = Boolean(params.bubbles); 100 | e.cancelable = Boolean(params.cancelable); 101 | e.detail = params.detail; 102 | } else { 103 | e.bubbles = false; 104 | e.cancelable = false; 105 | e.detail = void 0; 106 | } 107 | return e; 108 | } 109 | 110 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 111 | 112 | },{}],3:[function(require,module,exports){ 113 | (function (global){ 114 | 'use strict'; 115 | 116 | var customEvent = require('custom-event'); 117 | var eventmap = require('./eventmap'); 118 | var doc = global.document; 119 | var addEvent = addEventEasy; 120 | var removeEvent = removeEventEasy; 121 | var hardCache = []; 122 | 123 | if (!global.addEventListener) { 124 | addEvent = addEventHard; 125 | removeEvent = removeEventHard; 126 | } 127 | 128 | module.exports = { 129 | add: addEvent, 130 | remove: removeEvent, 131 | fabricate: fabricateEvent 132 | }; 133 | 134 | function addEventEasy (el, type, fn, capturing) { 135 | return el.addEventListener(type, fn, capturing); 136 | } 137 | 138 | function addEventHard (el, type, fn) { 139 | return el.attachEvent('on' + type, wrap(el, type, fn)); 140 | } 141 | 142 | function removeEventEasy (el, type, fn, capturing) { 143 | return el.removeEventListener(type, fn, capturing); 144 | } 145 | 146 | function removeEventHard (el, type, fn) { 147 | var listener = unwrap(el, type, fn); 148 | if (listener) { 149 | return el.detachEvent('on' + type, listener); 150 | } 151 | } 152 | 153 | function fabricateEvent (el, type, model) { 154 | var e = eventmap.indexOf(type) === -1 ? makeCustomEvent() : makeClassicEvent(); 155 | if (el.dispatchEvent) { 156 | el.dispatchEvent(e); 157 | } else { 158 | el.fireEvent('on' + type, e); 159 | } 160 | function makeClassicEvent () { 161 | var e; 162 | if (doc.createEvent) { 163 | e = doc.createEvent('Event'); 164 | e.initEvent(type, true, true); 165 | } else if (doc.createEventObject) { 166 | e = doc.createEventObject(); 167 | } 168 | return e; 169 | } 170 | function makeCustomEvent () { 171 | return new customEvent(type, { detail: model }); 172 | } 173 | } 174 | 175 | function wrapperFactory (el, type, fn) { 176 | return function wrapper (originalEvent) { 177 | var e = originalEvent || global.event; 178 | e.target = e.target || e.srcElement; 179 | e.preventDefault = e.preventDefault || function preventDefault () { e.returnValue = false; }; 180 | e.stopPropagation = e.stopPropagation || function stopPropagation () { e.cancelBubble = true; }; 181 | e.which = e.which || e.keyCode; 182 | fn.call(el, e); 183 | }; 184 | } 185 | 186 | function wrap (el, type, fn) { 187 | var wrapper = unwrap(el, type, fn) || wrapperFactory(el, type, fn); 188 | hardCache.push({ 189 | wrapper: wrapper, 190 | element: el, 191 | type: type, 192 | fn: fn 193 | }); 194 | return wrapper; 195 | } 196 | 197 | function unwrap (el, type, fn) { 198 | var i = find(el, type, fn); 199 | if (i) { 200 | var wrapper = hardCache[i].wrapper; 201 | hardCache.splice(i, 1); // free up a tad of memory 202 | return wrapper; 203 | } 204 | } 205 | 206 | function find (el, type, fn) { 207 | var i, item; 208 | for (i = 0; i < hardCache.length; i++) { 209 | item = hardCache[i]; 210 | if (item.element === el && item.type === type && item.fn === fn) { 211 | return i; 212 | } 213 | } 214 | } 215 | 216 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 217 | 218 | },{"./eventmap":4,"custom-event":2}],4:[function(require,module,exports){ 219 | (function (global){ 220 | 'use strict'; 221 | 222 | var eventmap = []; 223 | var eventname = ''; 224 | var ron = /^on/; 225 | 226 | for (eventname in global) { 227 | if (ron.test(eventname)) { 228 | eventmap.push(eventname.slice(2)); 229 | } 230 | } 231 | 232 | module.exports = eventmap; 233 | 234 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 235 | 236 | },{}]},{},[1]) 237 | //# sourceMappingURL=data:application/json;charset:utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9icm93c2VyaWZ5L25vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCJleGFtcGxlL2V4YW1wbGUuanMiLCJub2RlX21vZHVsZXMvY3Jvc3N2ZW50L25vZGVfbW9kdWxlcy9jdXN0b20tZXZlbnQvaW5kZXguanMiLCJub2RlX21vZHVsZXMvY3Jvc3N2ZW50L3NyYy9jcm9zc3ZlbnQuanMiLCJub2RlX21vZHVsZXMvY3Jvc3N2ZW50L3NyYy9ldmVudG1hcC5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtBQ0FBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7O0FDeERBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOzs7OztBQ2hEQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7Ozs7O0FDckdBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJmaWxlIjoiZ2VuZXJhdGVkLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXNDb250ZW50IjpbIihmdW5jdGlvbiBlKHQsbixyKXtmdW5jdGlvbiBzKG8sdSl7aWYoIW5bb10pe2lmKCF0W29dKXt2YXIgYT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2lmKCF1JiZhKXJldHVybiBhKG8sITApO2lmKGkpcmV0dXJuIGkobywhMCk7dmFyIGY9bmV3IEVycm9yKFwiQ2Fubm90IGZpbmQgbW9kdWxlICdcIitvK1wiJ1wiKTt0aHJvdyBmLmNvZGU9XCJNT0RVTEVfTk9UX0ZPVU5EXCIsZn12YXIgbD1uW29dPXtleHBvcnRzOnt9fTt0W29dWzBdLmNhbGwobC5leHBvcnRzLGZ1bmN0aW9uKGUpe3ZhciBuPXRbb11bMV1bZV07cmV0dXJuIHMobj9uOmUpfSxsLGwuZXhwb3J0cyxlLHQsbixyKX1yZXR1cm4gbltvXS5leHBvcnRzfXZhciBpPXR5cGVvZiByZXF1aXJlPT1cImZ1bmN0aW9uXCImJnJlcXVpcmU7Zm9yKHZhciBvPTA7bzxyLmxlbmd0aDtvKyspcyhyW29dKTtyZXR1cm4gc30pIiwiJ3VzZSBzdHJpY3QnO1xuXG52YXIgY3Jvc3N2ZW50ID0gcmVxdWlyZSgnY3Jvc3N2ZW50Jyk7XG52YXIgc29ydGFibGUgPSAkKCdzb3J0YWJsZScpO1xuXG5kcmFndWxhKFskKCdsZWZ0LWRlZmF1bHRzJyksICQoJ3JpZ2h0LWRlZmF1bHRzJyldKTtcbmRyYWd1bGEoWyQoJ2xlZnQtY29weScpLCAkKCdyaWdodC1jb3B5JyldLCB7IGNvcHk6IHRydWUgfSk7XG5kcmFndWxhKFskKCdsZWZ0LWV2ZW50cycpLCAkKCdyaWdodC1ldmVudHMnKV0pXG4gIC5vbignZHJhZycsIGZ1bmN0aW9uIChlbCkge1xuICAgIGVsLmNsYXNzTmFtZSA9IGVsLmNsYXNzTmFtZS5yZXBsYWNlKCdleC1tb3ZlZCcsICcnKTtcbiAgfSlcbiAgLm9uKCdkcm9wJywgZnVuY3Rpb24gKGVsKSB7XG4gICAgZWwuY2xhc3NOYW1lICs9ICcgZXgtbW92ZWQnO1xuICB9KVxuICAub24oJ292ZXInLCBmdW5jdGlvbiAoZWwsIGNvbnRhaW5lcikge1xuICAgIGNvbnRhaW5lci5jbGFzc05hbWUgKz0gJyBleC1vdmVyJztcbiAgfSlcbiAgLm9uKCdvdXQnLCBmdW5jdGlvbiAoZWwsIGNvbnRhaW5lcikge1xuICAgIGNvbnRhaW5lci5jbGFzc05hbWUgPSBjb250YWluZXIuY2xhc3NOYW1lLnJlcGxhY2UoJ2V4LW92ZXInLCAnJyk7XG4gIH0pO1xuZHJhZ3VsYShbJCgnbGVmdC1yb2xsYmFja3MnKSwgJCgncmlnaHQtcm9sbGJhY2tzJyldLCB7IHJldmVydE9uU3BpbGw6IHRydWUgfSk7XG5kcmFndWxhKFskKCdsZWZ0LWxvdmVoYW5kbGVzJyksICQoJ3JpZ2h0LWxvdmVoYW5kbGVzJyldLCB7XG4gIG1vdmVzOiBmdW5jdGlvbiAoZWwsIGNvbnRhaW5lciwgaGFuZGxlKSB7XG4gICAgcmV0dXJuIGhhbmRsZS5jbGFzc05hbWUgPT09ICdoYW5kbGUnO1xuICB9XG59KTtcblxuZHJhZ3VsYShbJCgnbGVmdC1ybS1zcGlsbCcpLCAkKCdyaWdodC1ybS1zcGlsbCcpXSwgeyByZW1vdmVPblNwaWxsOiB0cnVlIH0pO1xuZHJhZ3VsYShbJCgnbGVmdC1jb3B5LTF0b21hbnknKSwgJCgncmlnaHQtY29weS0xdG9tYW55JyldLCB7XG4gIGNvcHk6IGZ1bmN0aW9uIChlbCwgc291cmNlKSB7XG4gICAgcmV0dXJuIHNvdXJjZSA9PT0gJCgnbGVmdC1jb3B5LTF0b21hbnknKTtcbiAgfSxcbiAgYWNjZXB0czogZnVuY3Rpb24gKGVsLCB0YXJnZXQpIHtcbiAgICByZXR1cm4gdGFyZ2V0ICE9PSAkKCdsZWZ0LWNvcHktMXRvbWFueScpO1xuICB9XG59KTtcblxuZHJhZ3VsYShbc29ydGFibGVdKTtcblxuY3Jvc3N2ZW50LmFkZChzb3J0YWJsZSwgJ2NsaWNrJywgY2xpY2tIYW5kbGVyKTtcblxuZnVuY3Rpb24gY2xpY2tIYW5kbGVyIChlKSB7XG4gIHZhciB0YXJnZXQgPSBlLnRhcmdldDtcbiAgaWYgKHRhcmdldCA9PT0gc29ydGFibGUpIHtcbiAgICByZXR1cm47XG4gIH1cbiAgdGFyZ2V0LmlubmVySFRNTCArPSAnIFtjbGljayFdJztcblxuICBzZXRUaW1lb3V0KGZ1bmN0aW9uICgpIHtcbiAgICB0YXJnZXQuaW5uZXJIVE1MID0gdGFyZ2V0LmlubmVySFRNTC5yZXBsYWNlKC8gXFxbY2xpY2shXFxdL2csICcnKTtcbiAgfSwgNTAwKTtcbn1cblxuZnVuY3Rpb24gJCAoaWQpIHtcbiAgcmV0dXJuIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGlkKTtcbn1cbiIsIlxudmFyIE5hdGl2ZUN1c3RvbUV2ZW50ID0gZ2xvYmFsLkN1c3RvbUV2ZW50O1xuXG5mdW5jdGlvbiB1c2VOYXRpdmUgKCkge1xuICB0cnkge1xuICAgIHZhciBwID0gbmV3IE5hdGl2ZUN1c3RvbUV2ZW50KCdjYXQnLCB7IGRldGFpbDogeyBmb286ICdiYXInIH0gfSk7XG4gICAgcmV0dXJuICAnY2F0JyA9PT0gcC50eXBlICYmICdiYXInID09PSBwLmRldGFpbC5mb287XG4gIH0gY2F0Y2ggKGUpIHtcbiAgfVxuICByZXR1cm4gZmFsc2U7XG59XG5cbi8qKlxuICogQ3Jvc3MtYnJvd3NlciBgQ3VzdG9tRXZlbnRgIGNvbnN0cnVjdG9yLlxuICpcbiAqIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0FQSS9DdXN0b21FdmVudC5DdXN0b21FdmVudFxuICpcbiAqIEBwdWJsaWNcbiAqL1xuXG5tb2R1bGUuZXhwb3J0cyA9IHVzZU5hdGl2ZSgpID8gTmF0aXZlQ3VzdG9tRXZlbnQgOlxuXG4vLyBJRSA+PSA5XG4nZnVuY3Rpb24nID09PSB0eXBlb2YgZG9jdW1lbnQuY3JlYXRlRXZlbnQgPyBmdW5jdGlvbiBDdXN0b21FdmVudCAodHlwZSwgcGFyYW1zKSB7XG4gIHZhciBlID0gZG9jdW1lbnQuY3JlYXRlRXZlbnQoJ0N1c3RvbUV2ZW50Jyk7XG4gIGlmIChwYXJhbXMpIHtcbiAgICBlLmluaXRDdXN0b21FdmVudCh0eXBlLCBwYXJhbXMuYnViYmxlcywgcGFyYW1zLmNhbmNlbGFibGUsIHBhcmFtcy5kZXRhaWwpO1xuICB9IGVsc2Uge1xuICAgIGUuaW5pdEN1c3RvbUV2ZW50KHR5cGUsIGZhbHNlLCBmYWxzZSwgdm9pZCAwKTtcbiAgfVxuICByZXR1cm4gZTtcbn0gOlxuXG4vLyBJRSA8PSA4XG5mdW5jdGlvbiBDdXN0b21FdmVudCAodHlwZSwgcGFyYW1zKSB7XG4gIHZhciBlID0gZG9jdW1lbnQuY3JlYXRlRXZlbnRPYmplY3QoKTtcbiAgZS50eXBlID0gdHlwZTtcbiAgaWYgKHBhcmFtcykge1xuICAgIGUuYnViYmxlcyA9IEJvb2xlYW4ocGFyYW1zLmJ1YmJsZXMpO1xuICAgIGUuY2FuY2VsYWJsZSA9IEJvb2xlYW4ocGFyYW1zLmNhbmNlbGFibGUpO1xuICAgIGUuZGV0YWlsID0gcGFyYW1zLmRldGFpbDtcbiAgfSBlbHNlIHtcbiAgICBlLmJ1YmJsZXMgPSBmYWxzZTtcbiAgICBlLmNhbmNlbGFibGUgPSBmYWxzZTtcbiAgICBlLmRldGFpbCA9IHZvaWQgMDtcbiAgfVxuICByZXR1cm4gZTtcbn1cbiIsIid1c2Ugc3RyaWN0JztcblxudmFyIGN1c3RvbUV2ZW50ID0gcmVxdWlyZSgnY3VzdG9tLWV2ZW50Jyk7XG52YXIgZXZlbnRtYXAgPSByZXF1aXJlKCcuL2V2ZW50bWFwJyk7XG52YXIgZG9jID0gZ2xvYmFsLmRvY3VtZW50O1xudmFyIGFkZEV2ZW50ID0gYWRkRXZlbnRFYXN5O1xudmFyIHJlbW92ZUV2ZW50ID0gcmVtb3ZlRXZlbnRFYXN5O1xudmFyIGhhcmRDYWNoZSA9IFtdO1xuXG5pZiAoIWdsb2JhbC5hZGRFdmVudExpc3RlbmVyKSB7XG4gIGFkZEV2ZW50ID0gYWRkRXZlbnRIYXJkO1xuICByZW1vdmVFdmVudCA9IHJlbW92ZUV2ZW50SGFyZDtcbn1cblxubW9kdWxlLmV4cG9ydHMgPSB7XG4gIGFkZDogYWRkRXZlbnQsXG4gIHJlbW92ZTogcmVtb3ZlRXZlbnQsXG4gIGZhYnJpY2F0ZTogZmFicmljYXRlRXZlbnRcbn07XG5cbmZ1bmN0aW9uIGFkZEV2ZW50RWFzeSAoZWwsIHR5cGUsIGZuLCBjYXB0dXJpbmcpIHtcbiAgcmV0dXJuIGVsLmFkZEV2ZW50TGlzdGVuZXIodHlwZSwgZm4sIGNhcHR1cmluZyk7XG59XG5cbmZ1bmN0aW9uIGFkZEV2ZW50SGFyZCAoZWwsIHR5cGUsIGZuKSB7XG4gIHJldHVybiBlbC5hdHRhY2hFdmVudCgnb24nICsgdHlwZSwgd3JhcChlbCwgdHlwZSwgZm4pKTtcbn1cblxuZnVuY3Rpb24gcmVtb3ZlRXZlbnRFYXN5IChlbCwgdHlwZSwgZm4sIGNhcHR1cmluZykge1xuICByZXR1cm4gZWwucmVtb3ZlRXZlbnRMaXN0ZW5lcih0eXBlLCBmbiwgY2FwdHVyaW5nKTtcbn1cblxuZnVuY3Rpb24gcmVtb3ZlRXZlbnRIYXJkIChlbCwgdHlwZSwgZm4pIHtcbiAgdmFyIGxpc3RlbmVyID0gdW53cmFwKGVsLCB0eXBlLCBmbik7XG4gIGlmIChsaXN0ZW5lcikge1xuICAgIHJldHVybiBlbC5kZXRhY2hFdmVudCgnb24nICsgdHlwZSwgbGlzdGVuZXIpO1xuICB9XG59XG5cbmZ1bmN0aW9uIGZhYnJpY2F0ZUV2ZW50IChlbCwgdHlwZSwgbW9kZWwpIHtcbiAgdmFyIGUgPSBldmVudG1hcC5pbmRleE9mKHR5cGUpID09PSAtMSA/IG1ha2VDdXN0b21FdmVudCgpIDogbWFrZUNsYXNzaWNFdmVudCgpO1xuICBpZiAoZWwuZGlzcGF0Y2hFdmVudCkge1xuICAgIGVsLmRpc3BhdGNoRXZlbnQoZSk7XG4gIH0gZWxzZSB7XG4gICAgZWwuZmlyZUV2ZW50KCdvbicgKyB0eXBlLCBlKTtcbiAgfVxuICBmdW5jdGlvbiBtYWtlQ2xhc3NpY0V2ZW50ICgpIHtcbiAgICB2YXIgZTtcbiAgICBpZiAoZG9jLmNyZWF0ZUV2ZW50KSB7XG4gICAgICBlID0gZG9jLmNyZWF0ZUV2ZW50KCdFdmVudCcpO1xuICAgICAgZS5pbml0RXZlbnQodHlwZSwgdHJ1ZSwgdHJ1ZSk7XG4gICAgfSBlbHNlIGlmIChkb2MuY3JlYXRlRXZlbnRPYmplY3QpIHtcbiAgICAgIGUgPSBkb2MuY3JlYXRlRXZlbnRPYmplY3QoKTtcbiAgICB9XG4gICAgcmV0dXJuIGU7XG4gIH1cbiAgZnVuY3Rpb24gbWFrZUN1c3RvbUV2ZW50ICgpIHtcbiAgICByZXR1cm4gbmV3IGN1c3RvbUV2ZW50KHR5cGUsIHsgZGV0YWlsOiBtb2RlbCB9KTtcbiAgfVxufVxuXG5mdW5jdGlvbiB3cmFwcGVyRmFjdG9yeSAoZWwsIHR5cGUsIGZuKSB7XG4gIHJldHVybiBmdW5jdGlvbiB3cmFwcGVyIChvcmlnaW5hbEV2ZW50KSB7XG4gICAgdmFyIGUgPSBvcmlnaW5hbEV2ZW50IHx8IGdsb2JhbC5ldmVudDtcbiAgICBlLnRhcmdldCA9IGUudGFyZ2V0IHx8IGUuc3JjRWxlbWVudDtcbiAgICBlLnByZXZlbnREZWZhdWx0ID0gZS5wcmV2ZW50RGVmYXVsdCB8fCBmdW5jdGlvbiBwcmV2ZW50RGVmYXVsdCAoKSB7IGUucmV0dXJuVmFsdWUgPSBmYWxzZTsgfTtcbiAgICBlLnN0b3BQcm9wYWdhdGlvbiA9IGUuc3RvcFByb3BhZ2F0aW9uIHx8IGZ1bmN0aW9uIHN0b3BQcm9wYWdhdGlvbiAoKSB7IGUuY2FuY2VsQnViYmxlID0gdHJ1ZTsgfTtcbiAgICBlLndoaWNoID0gZS53aGljaCB8fCBlLmtleUNvZGU7XG4gICAgZm4uY2FsbChlbCwgZSk7XG4gIH07XG59XG5cbmZ1bmN0aW9uIHdyYXAgKGVsLCB0eXBlLCBmbikge1xuICB2YXIgd3JhcHBlciA9IHVud3JhcChlbCwgdHlwZSwgZm4pIHx8IHdyYXBwZXJGYWN0b3J5KGVsLCB0eXBlLCBmbik7XG4gIGhhcmRDYWNoZS5wdXNoKHtcbiAgICB3cmFwcGVyOiB3cmFwcGVyLFxuICAgIGVsZW1lbnQ6IGVsLFxuICAgIHR5cGU6IHR5cGUsXG4gICAgZm46IGZuXG4gIH0pO1xuICByZXR1cm4gd3JhcHBlcjtcbn1cblxuZnVuY3Rpb24gdW53cmFwIChlbCwgdHlwZSwgZm4pIHtcbiAgdmFyIGkgPSBmaW5kKGVsLCB0eXBlLCBmbik7XG4gIGlmIChpKSB7XG4gICAgdmFyIHdyYXBwZXIgPSBoYXJkQ2FjaGVbaV0ud3JhcHBlcjtcbiAgICBoYXJkQ2FjaGUuc3BsaWNlKGksIDEpOyAvLyBmcmVlIHVwIGEgdGFkIG9mIG1lbW9yeVxuICAgIHJldHVybiB3cmFwcGVyO1xuICB9XG59XG5cbmZ1bmN0aW9uIGZpbmQgKGVsLCB0eXBlLCBmbikge1xuICB2YXIgaSwgaXRlbTtcbiAgZm9yIChpID0gMDsgaSA8IGhhcmRDYWNoZS5sZW5ndGg7IGkrKykge1xuICAgIGl0ZW0gPSBoYXJkQ2FjaGVbaV07XG4gICAgaWYgKGl0ZW0uZWxlbWVudCA9PT0gZWwgJiYgaXRlbS50eXBlID09PSB0eXBlICYmIGl0ZW0uZm4gPT09IGZuKSB7XG4gICAgICByZXR1cm4gaTtcbiAgICB9XG4gIH1cbn1cbiIsIid1c2Ugc3RyaWN0JztcblxudmFyIGV2ZW50bWFwID0gW107XG52YXIgZXZlbnRuYW1lID0gJyc7XG52YXIgcm9uID0gL15vbi87XG5cbmZvciAoZXZlbnRuYW1lIGluIGdsb2JhbCkge1xuICBpZiAocm9uLnRlc3QoZXZlbnRuYW1lKSkge1xuICAgIGV2ZW50bWFwLnB1c2goZXZlbnRuYW1lLnNsaWNlKDIpKTtcbiAgfVxufVxuXG5tb2R1bGUuZXhwb3J0cyA9IGV2ZW50bWFwO1xuIl19 238 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bevacqua/dragula/09ab978dd550490f1f8e456551a1a39ed0d7f984/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | dragula 7 |

dragula

8 |

Drag and drop so simple it hurts 9 | 10 | Join us on Slack 11 | (get invite) 12 | 13 |

14 | 15 | Fork me on GitHub 16 | 17 |
18 |
19 | 20 |
21 |
22 |
You can move these elements between these two containers
23 |
Moving them anywhere else isn't quite possible
24 |
Anything can be moved around. That includes images, links, or any other nested elements. 25 |
dragula
(You can still click on links, as usual!) 26 |
27 |
28 |
29 |
There's also the possibility of moving elements around in the same container, changing their position
30 |
This is the default use case. You only need to specify the containers you want to use
31 |
More interactive use cases lie ahead
32 |
Moving <input/> elements works just fine. You can still focus them, too.
33 |
Make sure to check out the documentation on GitHub!
34 |
35 |
36 |
 37 |       
 38 | dragula([document.getElementById(left), document.getElementById(right)]);
 39 |       
 40 |     
41 |
42 |
43 | 44 |
45 |
46 |
As soon as you start dragging an element, a drag event is fired
47 |
Whenever an element is cloned because copy: true, a cloned event fires
48 |
The shadow event fires whenever the placeholder showing where an element would be dropped is moved to a different container or position
49 |
A drop event is fired whenever an element is dropped anywhere other than its origin (where it was initially dragged from)
50 |
51 |
52 |
If the element gets removed from the DOM as a result of dropping outside of any containers, a remove event gets fired
53 |
A cancel event is fired when an element would be dropped onto an invalid target, but retains its original placement instead
54 |
The over event fires when you drag something over a container, and out fires when you drag it away from the container
55 |
Lastly, a dragend event is fired whenever a drag operation ends, regardless of whether it ends in a cancellation, removal, or drop
56 |
57 |
58 |
 59 |       
 60 | dragula([document.getElementById(left), document.getElementById(right)])
 61 |   .on('drag', function (el) {
 62 |     el.className = el.className.replace('ex-moved', '');
 63 |   }).on('drop', function (el) {
 64 |     el.className += ' ex-moved';
 65 |   }).on('over', function (el, container) {
 66 |     container.className += ' ex-over';
 67 |   }).on('out', function (el, container) {
 68 |     container.className = container.className.replace('ex-over', '');
 69 |   });
 70 |       
 71 |     
72 |
73 |
74 | 75 |
76 |
77 |
Anxious Cab Driver
78 |
Thriving Venture
79 | 80 |
Calm Clam
81 |
82 |
83 |
Banana Boat
84 |
Orange Juice
85 |
Cuban Cigar
86 |
Terrible Comedian
87 |
88 |
89 |
 90 |       
 91 | dragula([document.getElementById(single)], {
 92 |   removeOnSpill: true
 93 | });
 94 |       
 95 |     
96 |
97 |
98 | 99 |
100 |
101 |
Moving items between containers works as usual
102 |
If you try to drop an item outside of any containers, though, it'll retain its original position
103 |
When that happens, a cancel event will be raised
104 |
105 |
106 |
Note that the dragged element will go back to the place you originally dragged it from, even if you move it over other containers
107 |
This is useful if you want to ensure drop events only happen when the user intends for them to happen explicitly, avoiding surprises
108 |
109 |
110 |
111 |       
112 | dragula([document.getElementById(left), document.getElementById(right)], {
113 |   revertOnSpill: true
114 | });
115 |       
116 |     
117 |
118 |
119 | 120 |
121 |
122 |
When elements are copyable, they can't be sorted in their origin container
123 |
Copying prevents original elements from being dragged. A copy gets created and that gets dragged instead
124 |
Whenever that happens, a cloned event is raised
125 |
126 |
127 |
Note that the clones get destroyed if they're not dropped into another container
128 |
You'll be dragging a copy, so when they're dropped into another container you'll see the duplication.
129 |
130 |
131 |
132 |       
133 | dragula([document.getElementById(left), document.getElementById(right)], {
134 |   copy: true
135 | });
136 |       
137 |     
138 |
139 |
140 | 141 |
142 |
143 |
When elements are copyable, they can't be sorted in their origin container
144 |
Copying prevents original elements from being dragged. A copy gets created and that gets dragged instead
145 |
Whenever that happens, a cloned event is raised
146 |
Note that the clones get destroyed if they're not dropped into another container
147 |
You'll be dragging a copy, so when they're dropped into another container you'll see the duplication.
148 |
149 |
150 |
151 |
152 |
153 |       
154 | dragula([document.getElementById(left), document.getElementById(right)], {
155 |   copy: function (el, source) {
156 |     return source === document.getElementById(left)
157 |   },
158 |   accepts: function (el, target) {
159 |     return target !== document.getElementById(left)
160 |   }
161 | });
162 |       
163 |     
164 |
165 |
166 | 167 |
168 |
169 |
+Move me, but you can use the plus sign to drag me around.
170 |
+Note that handle element in the moves handler is just the original event target.
171 |
172 |
173 |
+This might also be useful if you want multiple children of an element to be able to trigger a drag event.
174 |
+You can also use the moves option to determine whether an element can be dragged at all from a container, drag handle or not.
175 |
176 |
177 |
178 |       
179 | dragula([document.getElementById(left), document.getElementById(right)], {
180 |   moves: function (el, container, handle) {
181 |     return handle.className === 'handle';
182 |   }
183 | });
184 |       
185 |     
186 |
There are a few similar mechanisms to determine whether an element can be dragged from a certain container (moves), whether an element can be dropped into a certain container at a certain position (accepts), and whether an element is able to originate a drag event (invalid).
187 |
188 |
189 | 190 |
191 |
192 |
Clicking on these elements triggers a regular click event you can listen to.
193 |
Try dragging or clicking on this element.
194 |
Note how you can click normally?
195 |
Drags don't trigger click events.
196 |
Clicks don't end up in a drag, either.
197 |
This is useful if you have elements that can be both clicked or dragged.
198 |
199 |
200 |
201 |       
202 |         dragula([document.getElementById(container)]);
203 |       
204 |     
205 |
206 |
207 |

Who couldn't love a pun that good? — The Next Web

208 |

Get it on GitHub! bevacqua/dragula

209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2015-2016 Nicolas Bevacqua 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dragula", 3 | "version": "3.7.3", 4 | "description": "Drag and drop so simple it hurts", 5 | "main": "dragula.js", 6 | "scripts": { 7 | "build": "npm run scripts && npm run styles", 8 | "deploy": "npm run build && npm run deployment && npm run sync", 9 | "deployment": "git add dist && npm version ${BUMP:-\"patch\"} && git add package.json && git commit -am \"Autogenerated pre-deployment commit\" && git reset HEAD~2 && git add . && git commit -am \"Release $(cat package.json | jq -r .version)\" && git push --tags && npm publish && git push", 10 | "scripts": "jshint . && browserify -s dragula -do dist/dragula.js dragula.js && uglifyjs -m -c -o dist/dragula.min.js dist/dragula.js", 11 | "start": "watchify -dvo example/example.min.js example/example.js & watchify -dvs dragula -o dist/dragula.js dragula.js & stylus -w dragula.styl --import node_modules/nib -o dist", 12 | "styles": "stylus dragula.styl --import node_modules/nib -o dist && cleancss dist/dragula.css -o dist/dragula.min.css", 13 | "sync": "git checkout gh-pages ; git merge master ; git push ; git checkout master", 14 | "lint": "jshint . --reporter node_modules/jshint-stylish/index.js", 15 | "test": "npm run lint && browserify test/*.js | tape-run", 16 | "test-watch": "hihat test/*.js -p tap-dev-tool" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/bevacqua/dragula.git" 21 | }, 22 | "author": "Nicolas Bevacqua (http://bevacqua.io/)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/bevacqua/dragula/issues" 26 | }, 27 | "homepage": "https://github.com/bevacqua/dragula", 28 | "dependencies": { 29 | "contra": "1.9.4", 30 | "crossvent": "1.5.5" 31 | }, 32 | "devDependencies": { 33 | "browserify": "16.5.2", 34 | "clean-css": "4.2.3", 35 | "clean-css-cli": "^4.3.0", 36 | "hihat": "2.6.4", 37 | "jshint": "2.12.0", 38 | "jshint-stylish": "2.2.1", 39 | "nib": "1.1.2", 40 | "stylus": "0.54.8", 41 | "tap-dev-tool": "1.3.0", 42 | "tape": "5.0.1", 43 | "tape-run": "8.0.0", 44 | "uglify-js": "3.11.0", 45 | "watchify": "3.11.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | [![logo.png][3]][2] 2 | 3 | > Drag and drop so simple it hurts 4 | 5 | Browser support includes every sane browser and **IE7+**. _(Granted you polyfill the functional `Array` methods in ES5)_ 6 | 7 | Framework support includes vanilla JavaScript, Angular, and React. 8 | 9 | - Official [Angular bridge][8] for `dragula` [_(demo)_][10] 10 | - Official [Angular 2 bridge][22] for `dragula` [_(demo)_][23] 11 | - Official [React bridge][9] for `dragula` [_(demo)_][11] 12 | 13 | # Demo 14 | 15 | [![demo.png][1]][2] 16 | 17 | Try out the [demo][2]! 18 | 19 | # Inspiration 20 | 21 | Have you ever wanted a drag and drop library that just works? That doesn't just depend on bloated frameworks, that has great support? That actually understands where to place the elements when they are dropped? That doesn't need you to do a zillion things to get it to work? Well, so did I! 22 | 23 | # Features 24 | 25 | - Super easy to set up 26 | - No bloated dependencies 27 | - **Figures out sort order** on its own 28 | - A shadow where the item would be dropped offers **visual feedback** 29 | - Touch events! 30 | - Seamlessly handles clicks *without any configuration* 31 | 32 | # Install 33 | 34 | You can get it on npm. 35 | 36 | ```shell 37 | npm install dragula --save 38 | ``` 39 | 40 | Or a CDN. 41 | 42 | ```shell 43 | 44 | ``` 45 | 46 | If you're not using either package manager, you can use `dragula` by downloading the [files in the `dist` folder][15]. We **strongly suggest** using `npm`, though. 47 | 48 | ##### Including the JavaScript 49 | 50 | There's a caveat to `dragula`. You shouldn't include it in the `` of your web applications. It's bad practice to place scripts in the ``, and as such `dragula` makes no effort to support this use case. 51 | 52 | Place `dragula` in the ``, instead. 53 | 54 | ##### Including the CSS! 55 | 56 | There's [a few CSS styles][16] you need to incorporate in order for `dragula` to work as expected. 57 | 58 | You can add them by including [`dist/dragula.css`][12] or [`dist/dragula.min.css`][13] in your document. If you're using Stylus, you can include the styles using the directive below. 59 | 60 | ```styl 61 | @import 'node_modules/dragula/dragula' 62 | ``` 63 | 64 | # Usage 65 | 66 | Dragula provides the easiest possible API to make drag and drop a breeze in your applications. 67 | 68 | ## `dragula(containers?, options?)` 69 | 70 | By default, `dragula` will allow the user to drag an element in any of the `containers` and drop it in any other container in the list. If the element is dropped anywhere that's not one of the `containers`, the event will be gracefully cancelled according to the `revertOnSpill` and `removeOnSpill` options. 71 | 72 | Note that dragging is only triggered on left clicks, and only if no meta keys are pressed. 73 | 74 | The example below allows the user to drag elements from `left` into `right`, and from `right` into `left`. 75 | 76 | ```js 77 | dragula([document.querySelector('#left'), document.querySelector('#right')]); 78 | ``` 79 | 80 | You can also provide an `options` object. Here's an **overview of the default values**. 81 | 82 | ```js 83 | dragula(containers, { 84 | isContainer: function (el) { 85 | return false; // only elements in drake.containers will be taken into account 86 | }, 87 | moves: function (el, source, handle, sibling) { 88 | return true; // elements are always draggable by default 89 | }, 90 | accepts: function (el, target, source, sibling) { 91 | return true; // elements can be dropped in any of the `containers` by default 92 | }, 93 | invalid: function (el, handle) { 94 | return false; // don't prevent any drags from initiating by default 95 | }, 96 | direction: 'vertical', // Y axis is considered when determining where an element would be dropped 97 | copy: false, // elements are moved by default, not copied 98 | copySortSource: false, // elements in copy-source containers can be reordered 99 | revertOnSpill: false, // spilling will put the element back where it was dragged from, if this is true 100 | removeOnSpill: false, // spilling will `.remove` the element, if this is true 101 | mirrorContainer: document.body, // set the element that gets mirror elements appended 102 | ignoreInputTextSelection: true, // allows users to select input text, see details below 103 | slideFactorX: 0, // allows users to select the amount of movement on the X axis before it is considered a drag instead of a click 104 | slideFactorY: 0, // allows users to select the amount of movement on the Y axis before it is considered a drag instead of a click 105 | }); 106 | ``` 107 | 108 | You can omit the `containers` argument and add containers dynamically later on. 109 | 110 | ```js 111 | var drake = dragula({ 112 | copy: true 113 | }); 114 | drake.containers.push(container); 115 | ``` 116 | 117 | You can also set the `containers` from the `options` object. 118 | 119 | ```js 120 | var drake = dragula({ containers: containers }); 121 | ``` 122 | 123 | And you could also not set any arguments, which defaults to a drake without containers and with the default options. 124 | 125 | ```js 126 | var drake = dragula(); 127 | ``` 128 | 129 | The options are detailed below. 130 | 131 | #### `options.containers` 132 | 133 | Setting this option is effectively the same as passing the containers in the first argument to `dragula(containers, options)`. 134 | 135 | #### `options.isContainer` 136 | 137 | Besides the containers that you pass to `dragula`, or the containers you dynamically `push` or `unshift` from [drake.containers](#drakecontainers), you can also use this method to specify any sort of logic that defines what is a container for this particular `drake` instance. 138 | 139 | The example below dynamically treats all DOM elements with a CSS class of `dragula-container` as dragula containers for this `drake`. 140 | 141 | ```js 142 | var drake = dragula({ 143 | isContainer: function (el) { 144 | return el.classList.contains('dragula-container'); 145 | } 146 | }); 147 | ``` 148 | 149 | #### `options.moves` 150 | 151 | You can define a `moves` method which will be invoked with `(el, source, handle, sibling)` whenever an element is clicked. If this method returns `false`, a drag event won't begin, and the event won't be prevented either. The `handle` element will be the original click target, which comes in handy to test if that element is an expected _"drag handle"_. 152 | 153 | #### `options.accepts` 154 | 155 | You can set `accepts` to a method with the following signature: `(el, target, source, sibling)`. It'll be called to make sure that an element `el`, that came from container `source`, can be dropped on container `target` before a `sibling` element. The `sibling` can be `null`, which would mean that the element would be placed as the last element in the container. Note that if `options.copy` is set to `true`, `el` will be set to the copy, instead of the originally dragged element. 156 | 157 | Also note that **the position where a drag starts is always going to be a valid place where to drop the element**, even if `accepts` returned `false` for all cases. 158 | 159 | #### `options.copy` 160 | 161 | If `copy` is set to `true` _(or a method that returns `true`)_, items will be copied rather than moved. This implies the following differences: 162 | 163 | Event | Move | Copy 164 | ----------|------------------------------------------|--------------------------------------------- 165 | `drag` | Element will be concealed from `source` | Nothing happens 166 | `drop` | Element will be moved into `target` | Element will be cloned into `target` 167 | `remove` | Element will be removed from DOM | Nothing happens 168 | `cancel` | Element will stay in `source` | Nothing happens 169 | 170 | If a method is passed, it'll be called whenever an element starts being dragged in order to decide whether it should follow `copy` behavior or not. Consider the following example. 171 | 172 | ```js 173 | copy: function (el, source) { 174 | return el.className === 'you-may-copy-us'; 175 | } 176 | ``` 177 | #### `options.copySortSource` 178 | 179 | If `copy` is set to `true` _(or a method that returns `true`)_ and `copySortSource` is `true` as well, users will be able to sort elements in `copy`-source containers. 180 | 181 | ```js 182 | copy: true, 183 | copySortSource: true 184 | ``` 185 | 186 | #### `options.revertOnSpill` 187 | 188 | By default, spilling an element outside of any containers will move the element back to the _drop position previewed by the feedback shadow_. Setting `revertOnSpill` to `true` will ensure elements dropped outside of any approved containers are moved back to the source element where the drag event began, rather than stay at the _drop position previewed by the feedback shadow_. 189 | 190 | #### `options.removeOnSpill` 191 | 192 | By default, spilling an element outside of any containers will move the element back to the _drop position previewed by the feedback shadow_. Setting `removeOnSpill` to `true` will ensure elements dropped outside of any approved containers are removed from the DOM. Note that `remove` events won't fire if `copy` is set to `true`. 193 | 194 | #### `options.direction` 195 | 196 | When an element is dropped onto a container, it'll be placed near the point where the mouse was released. If the `direction` is `'vertical'`, the default value, the Y axis will be considered. Otherwise, if the `direction` is `'horizontal'`, the X axis will be considered. 197 | 198 | #### `options.invalid` 199 | 200 | You can provide an `invalid` method with a `(el, handle)` signature. This method should return `true` for elements that shouldn't trigger a drag. The `handle` argument is the element that was clicked, while `el` is the item that would be dragged. Here's the default implementation, which doesn't prevent any drags. 201 | 202 | ```js 203 | function invalidTarget (el, handle) { 204 | return false; 205 | } 206 | ``` 207 | 208 | Note that `invalid` will be invoked on the DOM element that was clicked and every parent up to immediate children of a `drake` container. 209 | 210 | As an example, you could set `invalid` to return `false` whenever the clicked element _(or any of its parents)_ is an anchor tag. 211 | 212 | ```js 213 | invalid: function (el, handle) { 214 | return el.tagName === 'A'; 215 | } 216 | ``` 217 | 218 | #### `options.mirrorContainer` 219 | 220 | The DOM element where the mirror element displayed while dragging will be appended to. Defaults to `document.body`. 221 | 222 | #### `options.ignoreInputTextSelection` 223 | 224 | When this option is enabled, if the user clicks on an input element the drag won't start until their mouse pointer exits the input. This translates into the user being able to select text in inputs contained inside draggable elements, and still drag the element by moving their mouse outside of the input -- so you get the best of both worlds. 225 | 226 | This option is enabled by default. Turn it off by setting it to `false`. If its disabled your users won't be able to select text in inputs within `dragula` containers with their mouse. 227 | 228 | ## API 229 | 230 | The `dragula` method returns a tiny object with a concise API. We'll refer to the API returned by `dragula` as `drake`. 231 | 232 | #### `drake.containers` 233 | 234 | This property contains the collection of containers that was passed to `dragula` when building this `drake` instance. You can `push` more containers and `splice` old containers at will. 235 | 236 | #### `drake.dragging` 237 | 238 | This property will be `true` whenever an element is being dragged. 239 | 240 | #### `drake.start(item)` 241 | 242 | Enter drag mode **without a shadow**. This method is most useful when providing complementary keyboard shortcuts to an existing drag and drop solution. Even though a shadow won't be created at first, the user will get one as soon as they click on `item` and start dragging it around. Note that if they click and drag something else, `.end` will be called before picking up the new item. 243 | 244 | #### `drake.end()` 245 | 246 | Gracefully end the drag event as if using **the last position marked by the preview shadow** as the drop target. The proper `cancel` or `drop` event will be fired, depending on whether the item was dropped back where it was originally lifted from _(which is essentially a no-op that's treated as a `cancel` event)_. 247 | 248 | #### `drake.cancel(revert)` 249 | 250 | If an element managed by `drake` is currently being dragged, this method will gracefully cancel the drag action. You can also pass in `revert` at the method invocation level, effectively producing the same result as if `revertOnSpill` was `true`. 251 | 252 | Note that **a _"cancellation"_ will result in a `cancel` event** only in the following scenarios. 253 | 254 | - `revertOnSpill` is `true` 255 | - Drop target _(as previewed by the feedback shadow)_ is the source container **and** the item is dropped in the same position where it was originally dragged from 256 | 257 | #### `drake.remove()` 258 | 259 | If an element managed by `drake` is currently being dragged, this method will gracefully remove it from the DOM. 260 | 261 | #### `drake.on` _(Events)_ 262 | 263 | The `drake` is an event emitter. The following events can be tracked using `drake.on(type, listener)`: 264 | 265 | Event Name | Listener Arguments | Event Description 266 | -----------|----------------------------------|------------------------------------------------------------------------------------- 267 | `drag` | `el, source` | `el` was lifted from `source` 268 | `dragend` | `el` | Dragging event for `el` ended with either `cancel`, `remove`, or `drop` 269 | `drop` | `el, target, source, sibling` | `el` was dropped into `target` before a `sibling` element, and originally came from `source` 270 | `cancel` | `el, container, source` | `el` was being dragged but it got nowhere and went back into `container`, its last stable parent; `el` originally came from `source` 271 | `remove` | `el, container, source` | `el` was being dragged but it got nowhere and it was removed from the DOM. Its last stable parent was `container`, and originally came from `source` 272 | `shadow` | `el, container, source` | `el`, _the visual aid shadow_, was moved into `container`. May trigger many times as the position of `el` changes, even within the same `container`; `el` originally came from `source` 273 | `over` | `el, container, source` | `el` is over `container`, and originally came from `source` 274 | `out` | `el, container, source` | `el` was dragged out of `container` or dropped, and originally came from `source` 275 | `cloned` | `clone, original, type` | DOM element `original` was cloned as `clone`, of `type` _(`'mirror'` or `'copy'`)_. Fired for mirror images and when `copy: true` 276 | 277 | #### `drake.canMove(item)` 278 | 279 | Returns whether the `drake` instance can accept drags for a DOM element `item`. This method returns `true` when all the conditions outlined below are met, and `false` otherwise. 280 | 281 | - `item` is a child of one of the specified containers for `drake` 282 | - `item` passes the pertinent [`invalid`](#optionsinvalid) checks 283 | - `item` passes a `moves` check 284 | 285 | #### `drake.destroy()` 286 | 287 | Removes all drag and drop events used by `dragula` to manage drag and drop between the `containers`. If `.destroy` is called while an element is being dragged, the drag will be effectively cancelled. 288 | 289 | ## CSS 290 | 291 | Dragula uses only four CSS classes. Their purpose is quickly explained below, but you can check [`dist/dragula.css`][12] to see the corresponding CSS rules. 292 | 293 | - `gu-unselectable` is added to the `mirrorContainer` element when dragging. You can use it to style the `mirrorContainer` while something is being dragged. 294 | - `gu-transit` is added to the source element when its mirror image is dragged. It just adds opacity to it. 295 | - `gu-mirror` is added to the mirror image. It handles fixed positioning and `z-index` _(and removes any prior margins on the element)_. Note that the mirror image is appended to the `mirrorContainer`, not to its initial container. Keep that in mind when styling your elements with nested rules, like `.list .item { padding: 10px; }`. 296 | - `gu-hide` is a helper class to apply `display: none` to an element. 297 | 298 | # Contributing 299 | 300 | See [contributing.markdown][14] for details. 301 | 302 | # Support 303 | 304 | We have a [dedicated support channel in Slack][24]. See [this issue][21] to get an invite. Support requests won't be handled through the repository. 305 | 306 | # License 307 | 308 | MIT 309 | 310 | [1]: https://github.com/bevacqua/dragula/blob/master/resources/demo.png 311 | [2]: http://bevacqua.github.io/dragula/ 312 | [3]: https://github.com/bevacqua/dragula/blob/master/resources/logo.png 313 | [4]: https://travis-ci.org/bevacqua/dragula 314 | [5]: https://travis-ci.org/bevacqua/dragula.svg 315 | [6]: https://david-dm.org/bevacqua/dragula.svg 316 | [7]: https://david-dm.org/bevacqua/dragula 317 | [8]: https://github.com/bevacqua/angularjs-dragula 318 | [9]: https://github.com/bevacqua/react-dragula 319 | [10]: http://bevacqua.github.io/angularjs-dragula/ 320 | [11]: http://bevacqua.github.io/react-dragula/ 321 | [12]: https://github.com/bevacqua/dragula/blob/master/dist/dragula.css 322 | [13]: https://github.com/bevacqua/dragula/blob/master/dist/dragula.min.css 323 | [14]: https://github.com/bevacqua/dragula/blob/master/.github/contributing.markdown 324 | [15]: https://github.com/bevacqua/dragula/blob/master/dist 325 | [16]: #css 326 | [17]: https://david-dm.org/bevacqua/dragula/dev-status.svg 327 | [18]: https://david-dm.org/bevacqua/dragula#info=devDependencies 328 | [19]: https://rawgit.com/bevacqua/dragula/master/resources/patreon.svg 329 | [20]: https://patreon.com/bevacqua 330 | [21]: https://github.com/bevacqua/dragula/issues/248 331 | [22]: https://github.com/valor-software/ng2-dragula 332 | [23]: http://valor-software.com/ng2-dragula/index.html 333 | [24]: https://dragula.slack.com 334 | -------------------------------------------------------------------------------- /resources/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bevacqua/dragula/09ab978dd550490f1f8e456551a1a39ed0d7f984/resources/demo.png -------------------------------------------------------------------------------- /resources/eyes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bevacqua/dragula/09ab978dd550490f1f8e456551a1a39ed0d7f984/resources/eyes.png -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bevacqua/dragula/09ab978dd550490f1f8e456551a1a39ed0d7f984/resources/icon.png -------------------------------------------------------------------------------- /resources/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bevacqua/dragula/09ab978dd550490f1f8e456551a1a39ed0d7f984/resources/logo.png -------------------------------------------------------------------------------- /resources/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/patreon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/cancel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var dragula = require('..'); 5 | 6 | test('cancel does not throw when not dragging', function (t) { 7 | t.test('a single time', function once (st) { 8 | var drake = dragula(); 9 | st.doesNotThrow(function () { 10 | drake.cancel(); 11 | }, 'dragula ignores a single call to drake.cancel'); 12 | st.end(); 13 | }); 14 | t.test('multiple times', function once (st) { 15 | var drake = dragula(); 16 | st.doesNotThrow(function () { 17 | drake.cancel(); 18 | drake.cancel(); 19 | drake.cancel(); 20 | drake.cancel(); 21 | }, 'dragula ignores multiple calls to drake.cancel'); 22 | st.end(); 23 | }); 24 | t.end(); 25 | }); 26 | 27 | test('when dragging and cancel gets called, nothing happens', function (t) { 28 | var div = document.createElement('div'); 29 | var item = document.createElement('div'); 30 | var drake = dragula([div]); 31 | div.appendChild(item); 32 | document.body.appendChild(div); 33 | drake.start(item); 34 | drake.cancel(); 35 | t.equal(div.children.length, 1, 'nothing happens'); 36 | t.equal(drake.dragging, false, 'drake has stopped dragging'); 37 | t.end(); 38 | }); 39 | 40 | test('when dragging and cancel gets called, cancel event is emitted', function (t) { 41 | var div = document.createElement('div'); 42 | var item = document.createElement('div'); 43 | var drake = dragula([div]); 44 | div.appendChild(item); 45 | document.body.appendChild(div); 46 | drake.start(item); 47 | drake.on('cancel', cancel); 48 | drake.on('dragend', dragend); 49 | drake.cancel(); 50 | t.plan(3); 51 | t.end(); 52 | function dragend () { 53 | t.pass('dragend got called'); 54 | } 55 | function cancel (target, container) { 56 | t.equal(target, item, 'cancel was invoked with item'); 57 | t.equal(container, div, 'cancel was invoked with container'); 58 | } 59 | }); 60 | 61 | test('when dragging a copy and cancel gets called, default does not revert', function (t) { 62 | var div = document.createElement('div'); 63 | var div2 = document.createElement('div'); 64 | var item = document.createElement('div'); 65 | var drake = dragula([div, div2]); 66 | div.appendChild(item); 67 | document.body.appendChild(div); 68 | document.body.appendChild(div2); 69 | drake.start(item); 70 | div2.appendChild(item); 71 | drake.on('drop', drop); 72 | drake.on('dragend', dragend); 73 | drake.cancel(); 74 | t.plan(4); 75 | t.end(); 76 | function dragend () { 77 | t.pass('dragend got called'); 78 | } 79 | function drop (target, parent, source) { 80 | t.equal(target, item, 'drop was invoked with item'); 81 | t.equal(parent, div2, 'drop was invoked with final container'); 82 | t.equal(source, div, 'drop was invoked with source container'); 83 | } 84 | }); 85 | 86 | test('when dragging a copy and cancel gets called, revert is executed', function (t) { 87 | var div = document.createElement('div'); 88 | var div2 = document.createElement('div'); 89 | var item = document.createElement('div'); 90 | var drake = dragula([div, div2]); 91 | div.appendChild(item); 92 | document.body.appendChild(div); 93 | document.body.appendChild(div2); 94 | drake.start(item); 95 | div2.appendChild(item); 96 | drake.on('cancel', cancel); 97 | drake.on('dragend', dragend); 98 | drake.cancel(true); 99 | t.plan(3); 100 | t.end(); 101 | function dragend () { 102 | t.pass('dragend got called'); 103 | } 104 | function cancel (target, container) { 105 | t.equal(target, item, 'cancel was invoked with item'); 106 | t.equal(container, div, 'cancel was invoked with container'); 107 | } 108 | }); 109 | -------------------------------------------------------------------------------- /test/classes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var classes = require('../classes'); 5 | 6 | test('classes exports the expected api', function (t) { 7 | t.equal(typeof classes.add, 'function', 'classes.add is a method'); 8 | t.equal(typeof classes.rm, 'function', 'classes.rm is a method'); 9 | t.end(); 10 | }); 11 | 12 | test('classes can add a class', function (t) { 13 | var el = document.createElement('div'); 14 | classes.add(el, 'gu-foo'); 15 | t.equal(el.className, 'gu-foo', 'setting a class works'); 16 | t.end(); 17 | }); 18 | 19 | test('classes can add a class to an element that already has classes', function (t) { 20 | var el = document.createElement('div'); 21 | el.className = 'bar'; 22 | classes.add(el, 'gu-foo'); 23 | t.equal(el.className, 'bar gu-foo', 'appending a class works'); 24 | t.end(); 25 | }); 26 | 27 | test('classes.add is a no-op if class already is in element', function (t) { 28 | var el = document.createElement('div'); 29 | el.className = 'gu-foo'; 30 | classes.add(el, 'gu-foo'); 31 | t.equal(el.className, 'gu-foo', 'no-op as expected'); 32 | t.end(); 33 | }); 34 | 35 | test('classes can remove a class', function (t) { 36 | var el = document.createElement('div'); 37 | el.className = 'gu-foo'; 38 | classes.rm(el, 'gu-foo'); 39 | t.equal(el.className, '', 'removing a class works'); 40 | t.end(); 41 | }); 42 | 43 | test('classes can remove a class from a list on the right', function (t) { 44 | var el = document.createElement('div'); 45 | el.className = 'bar gu-foo'; 46 | classes.rm(el, 'gu-foo'); 47 | t.equal(el.className, 'bar', 'removing a class from the list works to the right'); 48 | t.end(); 49 | }); 50 | 51 | test('classes can remove a class from a list on the left', function (t) { 52 | var el = document.createElement('div'); 53 | el.className = 'gu-foo bar'; 54 | classes.rm(el, 'gu-foo'); 55 | t.equal(el.className, 'bar', 'removing a class from the list works to the left'); 56 | t.end(); 57 | }); 58 | 59 | test('classes can remove a class from a list on the middle', function (t) { 60 | var el = document.createElement('div'); 61 | el.className = 'foo gu-foo bar'; 62 | classes.rm(el, 'gu-foo'); 63 | t.equal(el.className, 'foo bar', 'removing a class from the list works to the middle'); 64 | t.end(); 65 | }); 66 | -------------------------------------------------------------------------------- /test/containers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var dragula = require('..'); 5 | 6 | test('drake defaults to no containers', function (t) { 7 | var drake = dragula(); 8 | t.ok(Array.isArray(drake.containers), 'drake.containers is an array'); 9 | t.equal(drake.containers.length, 0, 'drake.containers is empty'); 10 | t.end(); 11 | }); 12 | 13 | test('drake reads containers from array argument', function (t) { 14 | var el = document.createElement('div'); 15 | var containers = [el]; 16 | var drake = dragula(containers); 17 | t.equal(drake.containers, containers, 'drake.containers matches input'); 18 | t.equal(drake.containers.length, 1, 'drake.containers has one item'); 19 | t.end(); 20 | }); 21 | 22 | test('drake reads containers from array in options', function (t) { 23 | var el = document.createElement('div'); 24 | var containers = [el]; 25 | var drake = dragula({ containers: containers }); 26 | t.equal(drake.containers, containers, 'drake.containers matches input'); 27 | t.equal(drake.containers.length, 1, 'drake.containers has one item'); 28 | t.end(); 29 | }); 30 | 31 | test('containers in options take precedent', function (t) { 32 | var el = document.createElement('div'); 33 | var containers = [el]; 34 | var drake = dragula([], { containers: containers }); 35 | t.equal(drake.containers, containers, 'drake.containers matches input'); 36 | t.equal(drake.containers.length, 1, 'drake.containers has one item'); 37 | t.end(); 38 | }); 39 | -------------------------------------------------------------------------------- /test/defaults.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var dragula = require('..'); 5 | 6 | test('drake has sensible default options', function (t) { 7 | var options = {}; 8 | dragula(options); 9 | t.equal(typeof options.moves, 'function', 'options.moves defaults to a method'); 10 | t.equal(typeof options.accepts, 'function', 'options.accepts defaults to a method'); 11 | t.equal(typeof options.invalid, 'function', 'options.invalid defaults to a method'); 12 | t.equal(typeof options.isContainer, 'function', 'options.isContainer defaults to a method'); 13 | t.equal(options.copy, false, 'options.copy defaults to false'); 14 | t.equal(options.revertOnSpill, false, 'options.revertOnSpill defaults to false'); 15 | t.equal(options.removeOnSpill, false, 'options.removeOnSpill defaults to false'); 16 | t.equal(options.direction, 'vertical', 'options.direction defaults to \'vertical\''); 17 | t.equal(options.mirrorContainer, document.body, 'options.mirrorContainer defaults to an document.body'); 18 | t.end(); 19 | }); 20 | -------------------------------------------------------------------------------- /test/destroy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var dragula = require('..'); 5 | 6 | test('destroy does not throw when not dragging, destroyed, or whatever', function (t) { 7 | t.test('a single time', function once (st) { 8 | var drake = dragula(); 9 | st.doesNotThrow(function () { 10 | drake.destroy(); 11 | }, 'dragula bites into a single call to drake.destroy'); 12 | st.end(); 13 | }); 14 | t.test('multiple times', function once (st) { 15 | var drake = dragula(); 16 | st.doesNotThrow(function () { 17 | drake.destroy(); 18 | drake.destroy(); 19 | drake.destroy(); 20 | drake.destroy(); 21 | }, 'dragula bites into multiple calls to drake.destroy'); 22 | st.end(); 23 | }); 24 | t.end(); 25 | }); 26 | 27 | test('when dragging and destroy gets called, nothing happens', function (t) { 28 | var div = document.createElement('div'); 29 | var item = document.createElement('div'); 30 | var drake = dragula([div]); 31 | div.appendChild(item); 32 | document.body.appendChild(div); 33 | drake.start(item); 34 | drake.destroy(); 35 | t.equal(div.children.length, 1, 'nothing happens'); 36 | t.equal(drake.dragging, false, 'drake has stopped dragging'); 37 | t.end(); 38 | }); 39 | 40 | test('when dragging and destroy gets called, dragend event is emitted gracefully', function (t) { 41 | var div = document.createElement('div'); 42 | var item = document.createElement('div'); 43 | var drake = dragula([div]); 44 | div.appendChild(item); 45 | document.body.appendChild(div); 46 | drake.start(item); 47 | drake.on('dragend', dragend); 48 | drake.destroy(); 49 | t.plan(1); 50 | t.end(); 51 | function dragend () { 52 | t.pass('dragend got called'); 53 | } 54 | }); 55 | 56 | test('when dragging a copy and destroy gets called, default does not revert', function (t) { 57 | var div = document.createElement('div'); 58 | var div2 = document.createElement('div'); 59 | var item = document.createElement('div'); 60 | var drake = dragula([div, div2]); 61 | div.appendChild(item); 62 | document.body.appendChild(div); 63 | document.body.appendChild(div2); 64 | drake.start(item); 65 | div2.appendChild(item); 66 | drake.on('drop', drop); 67 | drake.on('dragend', dragend); 68 | drake.destroy(); 69 | t.plan(4); 70 | t.end(); 71 | function dragend () { 72 | t.pass('dragend got called'); 73 | } 74 | function drop (target, parent, source) { 75 | t.equal(target, item, 'drop was invoked with item'); 76 | t.equal(parent, div2, 'drop was invoked with final container'); 77 | t.equal(source, div, 'drop was invoked with source container'); 78 | } 79 | }); 80 | 81 | test('when dragging a copy and destroy gets called, revert is executed', function (t) { 82 | var div = document.createElement('div'); 83 | var div2 = document.createElement('div'); 84 | var item = document.createElement('div'); 85 | var drake = dragula([div, div2], { revertOnSpill: true }); 86 | div.appendChild(item); 87 | document.body.appendChild(div); 88 | document.body.appendChild(div2); 89 | drake.start(item); 90 | div2.appendChild(item); 91 | drake.on('cancel', cancel); 92 | drake.on('dragend', dragend); 93 | drake.destroy(); 94 | t.plan(3); 95 | t.end(); 96 | function dragend () { 97 | t.pass('dragend got called'); 98 | } 99 | function cancel (target, container) { 100 | t.equal(target, item, 'cancel was invoked with item'); 101 | t.equal(container, div, 'cancel was invoked with container'); 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /test/drag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var events = require('./lib/events'); 5 | var dragula = require('..'); 6 | 7 | test('drag event gets emitted when clicking an item', function (t) { 8 | testCase('works for left clicks', { which: 1 }); 9 | testCase('works for wheel clicks', { which: 1 }); 10 | testCase('works when clicking buttons by default', { which: 1 }, { tag: 'button', passes: true }); 11 | testCase('works when clicking anchors by default', { which: 1 }, { tag: 'a', passes: true }); 12 | testCase('fails for right clicks', { which: 2 }, { passes: false }); 13 | testCase('fails for meta-clicks', { which: 1, metaKey: true }, { passes: false }); 14 | testCase('fails for ctrl-clicks', { which: 1, ctrlKey: true }, { passes: false }); 15 | testCase('fails when clicking containers', { which: 1 }, { containerClick: true, passes: false }); 16 | testCase('fails whenever invalid returns true', { which: 1 }, { passes: false, dragulaOpts: { invalid: always } }); 17 | testCase('fails whenever moves returns false', { which: 1 }, { passes: false, dragulaOpts: { moves: never } }); 18 | t.end(); 19 | function testCase (desc, eventOptions, options) { 20 | t.test(desc, function subtest (st) { 21 | var o = options || {}; 22 | var div = document.createElement('div'); 23 | var item = document.createElement(o.tag || 'div'); 24 | var passes = o.passes !== false; 25 | var drake = dragula([div], o.dragulaOpts); 26 | div.appendChild(item); 27 | document.body.appendChild(div); 28 | drake.on('drag', drag); 29 | events.raise(o.containerClick ? div : item, 'mousedown', eventOptions); 30 | events.raise(o.containerClick ? div : item, 'mousemove'); 31 | st.plan(passes ? 4 : 1); 32 | st.equal(drake.dragging, passes, desc + ': final state is drake is ' + (passes ? '' : 'not ') + 'dragging'); 33 | st.end(); 34 | function drag (target, container) { 35 | st[passes ? 'pass' : 'fail'](desc + ': drag event was emitted synchronously'); 36 | st.equal(target, item, desc + ': first argument is selected item'); 37 | st.equal(container, div, desc + ': second argument is container'); 38 | } 39 | }); 40 | } 41 | }); 42 | 43 | test('when already dragging, mousedown/mousemove ends (cancels) previous drag', function (t) { 44 | var div = document.createElement('div'); 45 | var item1 = document.createElement('div'); 46 | var item2 = document.createElement('div'); 47 | var drake = dragula([div]); 48 | div.appendChild(item1); 49 | div.appendChild(item2); 50 | document.body.appendChild(div); 51 | drake.start(item1); 52 | drake.on('dragend', end); 53 | drake.on('cancel', cancel); 54 | drake.on('drag', drag); 55 | events.raise(item2, 'mousedown', { which: 1 }); 56 | events.raise(item2, 'mousemove', { which: 1 }); 57 | t.plan(7); 58 | t.equal(drake.dragging, true, 'final state is drake is dragging'); 59 | t.end(); 60 | function end (item) { 61 | t.equal(item, item1, 'dragend invoked with correct item'); 62 | } 63 | function cancel (item, source) { 64 | t.equal(item, item1, 'cancel invoked with correct item'); 65 | t.equal(source, div, 'cancel invoked with correct source'); 66 | } 67 | function drag (item, container) { 68 | t.pass('drag event was emitted synchronously'); 69 | t.equal(item, item2, 'first argument is selected item'); 70 | t.equal(container, div, 'second argument is container'); 71 | } 72 | }); 73 | 74 | test('when already dragged, ends (drops) previous drag', function (t) { 75 | var div = document.createElement('div'); 76 | var div2 = document.createElement('div'); 77 | var item1 = document.createElement('div'); 78 | var item2 = document.createElement('div'); 79 | var drake = dragula([div, div2]); 80 | div.appendChild(item1); 81 | div.appendChild(item2); 82 | document.body.appendChild(div); 83 | document.body.appendChild(div2); 84 | drake.start(item1); 85 | div2.appendChild(item1); 86 | drake.on('dragend', end); 87 | drake.on('drop', drop); 88 | drake.on('drag', drag); 89 | events.raise(item2, 'mousedown', { which: 1 }); 90 | events.raise(item2, 'mousemove', { which: 1 }); 91 | t.plan(8); 92 | t.equal(drake.dragging, true, 'final state is drake is dragging'); 93 | t.end(); 94 | function end (item) { 95 | t.equal(item, item1, 'dragend invoked with correct item'); 96 | } 97 | function drop (item, target, source) { 98 | t.equal(item, item1, 'drop invoked with correct item'); 99 | t.equal(source, div, 'drop invoked with correct source'); 100 | t.equal(target, div2, 'drop invoked with correct target'); 101 | } 102 | function drag (item, container) { 103 | t.pass('drag event was emitted synchronously'); 104 | t.equal(item, item2, 'first argument is selected item'); 105 | t.equal(container, div, 'second argument is container'); 106 | } 107 | }); 108 | 109 | test('when copying, emits cloned with the copy', function (t) { 110 | var div = document.createElement('div'); 111 | var item1 = document.createElement('div'); 112 | var item2 = document.createElement('span'); 113 | var drake = dragula([div], { copy: true }); 114 | item2.innerHTML = 'the force is with this one'; 115 | div.appendChild(item1); 116 | div.appendChild(item2); 117 | document.body.appendChild(div); 118 | drake.start(item1); 119 | drake.on('cloned', cloned); 120 | drake.on('drag', drag); 121 | events.raise(item2, 'mousedown', { which: 1 }); 122 | events.raise(item2, 'mousemove', { which: 1 }); 123 | t.plan(12); 124 | t.equal(drake.dragging, true, 'final state is drake is dragging'); 125 | t.end(); 126 | function cloned (copy, item) { 127 | t.notEqual(copy, item2, 'first argument is not exactly the target'); 128 | t.equal(copy.tagName, item2.tagName, 'first argument has same tag as target'); 129 | t.equal(copy.innerHTML, item2.innerHTML, 'first argument has same inner html as target'); 130 | t.equal(item, item2, 'second argument is clicked item'); 131 | } 132 | function drag (item, container) { 133 | t.pass('drag event was emitted synchronously'); 134 | t.equal(item, item2, 'first argument is selected item'); 135 | t.equal(container, div, 'second argument is container'); 136 | } 137 | }); 138 | 139 | test('when dragging, element gets gu-transit class', function (t) { 140 | var div = document.createElement('div'); 141 | var item = document.createElement('div'); 142 | dragula([div]); 143 | div.appendChild(item); 144 | document.body.appendChild(div); 145 | events.raise(item, 'mousedown', { which: 1 }); 146 | events.raise(item, 'mousemove', { which: 1 }); 147 | t.equal(item.className, 'gu-transit', 'item has gu-transit class'); 148 | t.end(); 149 | }); 150 | 151 | test('when dragging, body gets gu-unselectable class', function (t) { 152 | var div = document.createElement('div'); 153 | var item = document.createElement('div'); 154 | dragula([div]); 155 | div.appendChild(item); 156 | document.body.appendChild(div); 157 | events.raise(item, 'mousedown', { which: 1 }); 158 | events.raise(item, 'mousemove', { which: 1 }); 159 | t.equal(document.body.className, 'gu-unselectable', 'body has gu-unselectable class'); 160 | t.end(); 161 | }); 162 | 163 | test('when dragging, element gets a mirror image for show', function (t) { 164 | var div = document.createElement('div'); 165 | var item = document.createElement('div'); 166 | var drake = dragula([div]); 167 | item.innerHTML = 'the force is with this one'; 168 | div.appendChild(item); 169 | document.body.appendChild(div); 170 | drake.on('cloned', cloned); 171 | events.raise(item, 'mousedown', { which: 1 }); 172 | events.raise(item, 'mousemove', { which: 1 }); 173 | t.plan(4); 174 | t.end(); 175 | function cloned (mirror, target) { 176 | t.equal(item.className, 'gu-transit', 'item does not have gu-mirror class'); 177 | t.equal(mirror.className, 'gu-mirror', 'mirror only has gu-mirror class'); 178 | t.equal(mirror.innerHTML, item.innerHTML, 'mirror is passed to \'cloned\' event'); 179 | t.equal(target, item, 'cloned lets you know that the mirror is a clone of `item`'); 180 | } 181 | }); 182 | 183 | test('when dragging, mirror element gets appended to configured mirrorContainer', function (t) { 184 | var mirrorContainer = document.createElement('div'); 185 | var div = document.createElement('div'); 186 | var item = document.createElement('div'); 187 | var drake = dragula([div], { 188 | 'mirrorContainer': mirrorContainer 189 | }); 190 | item.innerHTML = 'the force is with this one'; 191 | div.appendChild(item); 192 | document.body.appendChild(div); 193 | drake.on('cloned', cloned); 194 | events.raise(item, 'mousedown', { which: 1 }); 195 | events.raise(item, 'mousemove', { which: 1 }); 196 | t.plan(1); 197 | t.end(); 198 | function cloned (mirror) { 199 | t.equal(mirror.parentNode, mirrorContainer, 'mirrors parent is the configured mirrorContainer'); 200 | } 201 | }); 202 | 203 | test('when dragging stops, element gets gu-transit class removed', function (t) { 204 | var div = document.createElement('div'); 205 | var item = document.createElement('div'); 206 | var drake = dragula([div]); 207 | div.appendChild(item); 208 | document.body.appendChild(div); 209 | events.raise(item, 'mousedown', { which: 1 }); 210 | events.raise(item, 'mousemove', { which: 1 }); 211 | t.equal(item.className, 'gu-transit', 'item has gu-transit class'); 212 | drake.end(); 213 | t.equal(item.className, '', 'item has gu-transit class removed'); 214 | t.end(); 215 | }); 216 | 217 | test('when dragging stops, body becomes selectable again', function (t) { 218 | var div = document.createElement('div'); 219 | var item = document.createElement('div'); 220 | var drake = dragula([div]); 221 | div.appendChild(item); 222 | document.body.appendChild(div); 223 | events.raise(item, 'mousedown', { which: 1 }); 224 | events.raise(item, 'mousemove', { which: 1 }); 225 | t.equal(document.body.className, 'gu-unselectable', 'body has gu-unselectable class'); 226 | drake.end(); 227 | t.equal(document.body.className, '', 'body got gu-unselectable class removed'); 228 | t.end(); 229 | }); 230 | 231 | test('when drag begins, check for copy option', function (t) { 232 | var div = document.createElement('div'); 233 | var item = document.createElement('div'); 234 | item.className = 'copyable'; 235 | div.className = 'contains'; 236 | var drake = dragula([div], { 237 | copy: checkCondition 238 | }); 239 | item.innerHTML = 'the force is with this one'; 240 | div.appendChild(item); 241 | document.body.appendChild(div); 242 | events.raise(item, 'mousedown', { which: 1 }); 243 | events.raise(item, 'mousemove', { which: 1 }); 244 | events.raise(item, 'mousemove', { which: 1 }); // ensure the copy method condition is only asserted once 245 | t.plan(2); 246 | t.end(); 247 | function checkCondition (el, source) { 248 | t.equal(el.className, 'copyable', 'dragged element classname is copyable'); 249 | t.equal(source.className, 'contains', 'source container classname is contains'); 250 | return true; 251 | } 252 | drake.end(); 253 | }); 254 | 255 | function always () { return true; } 256 | function never () { return false; } 257 | -------------------------------------------------------------------------------- /test/drake-api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var dragula = require('..'); 5 | 6 | test('drake can be instantiated without throwing', function (t) { 7 | t.doesNotThrow(drakeFactory, 'calling dragula() without arguments does not throw'); 8 | t.end(); 9 | function drakeFactory () { 10 | return dragula(); 11 | } 12 | }); 13 | 14 | test('drake has expected api properties', function (t) { 15 | var drake = dragula(); 16 | t.ok(drake, 'drake is not null'); 17 | t.equal(typeof drake, 'object', 'drake is an object'); 18 | t.ok(Array.isArray(drake.containers), 'drake.containers is an array'); 19 | t.equal(typeof drake.start, 'function', 'drake.start is a method'); 20 | t.equal(typeof drake.end, 'function', 'drake.end is a method'); 21 | t.equal(typeof drake.cancel, 'function', 'drake.cancel is a method'); 22 | t.equal(typeof drake.remove, 'function', 'drake.remove is a method'); 23 | t.equal(typeof drake.destroy, 'function', 'drake.destroy is a method'); 24 | t.equal(typeof drake.dragging, 'boolean', 'drake.dragging is a boolean'); 25 | t.equal(drake.dragging, false, 'drake.dragging is initialized as false'); 26 | t.end(); 27 | }); 28 | -------------------------------------------------------------------------------- /test/end.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var dragula = require('..'); 5 | 6 | test('end does not throw when not dragging', function (t) { 7 | t.test('a single time', function once (st) { 8 | var drake = dragula(); 9 | st.doesNotThrow(function () { 10 | drake.end(); 11 | }, 'dragula ignores a single call to drake.end'); 12 | st.end(); 13 | }); 14 | t.test('multiple times', function once (st) { 15 | var drake = dragula(); 16 | st.doesNotThrow(function () { 17 | drake.end(); 18 | drake.end(); 19 | drake.end(); 20 | drake.end(); 21 | }, 'dragula ignores multiple calls to drake.end'); 22 | st.end(); 23 | }); 24 | t.end(); 25 | }); 26 | 27 | test('when already dragging, .end() ends (cancels) previous drag', function (t) { 28 | var div = document.createElement('div'); 29 | var item1 = document.createElement('div'); 30 | var item2 = document.createElement('div'); 31 | var drake = dragula([div]); 32 | div.appendChild(item1); 33 | div.appendChild(item2); 34 | document.body.appendChild(div); 35 | drake.start(item1); 36 | drake.on('dragend', end); 37 | drake.on('cancel', cancel); 38 | drake.end(); 39 | t.plan(4); 40 | t.equal(drake.dragging, false, 'final state is: drake is not dragging'); 41 | t.end(); 42 | function end (item) { 43 | t.equal(item, item1, 'dragend invoked with correct item'); 44 | } 45 | function cancel (item, source) { 46 | t.equal(item, item1, 'cancel invoked with correct item'); 47 | t.equal(source, div, 'cancel invoked with correct source'); 48 | } 49 | }); 50 | 51 | test('when already dragged, ends (drops) previous drag', function (t) { 52 | var div = document.createElement('div'); 53 | var div2 = document.createElement('div'); 54 | var item1 = document.createElement('div'); 55 | var item2 = document.createElement('div'); 56 | var drake = dragula([div, div2]); 57 | div.appendChild(item1); 58 | div.appendChild(item2); 59 | document.body.appendChild(div); 60 | document.body.appendChild(div2); 61 | drake.start(item1); 62 | div2.appendChild(item1); 63 | drake.on('dragend', end); 64 | drake.on('drop', drop); 65 | drake.end(); 66 | t.plan(5); 67 | t.equal(drake.dragging, false, 'final state is: drake is not dragging'); 68 | t.end(); 69 | function end (item) { 70 | t.equal(item, item1, 'dragend invoked with correct item'); 71 | } 72 | function drop (item, target, source) { 73 | t.equal(item, item1, 'drop invoked with correct item'); 74 | t.equal(source, div, 'drop invoked with correct source'); 75 | t.equal(target, div2, 'drop invoked with correct target'); 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /test/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var events = require('./lib/events'); 5 | var dragula = require('..'); 6 | 7 | test('.start() emits "cloned" for copies', function (t) { 8 | var div = document.createElement('div'); 9 | var item = document.createElement('div'); 10 | var drake = dragula([div], { copy: true }); 11 | div.appendChild(item); 12 | document.body.appendChild(div); 13 | drake.on('cloned', cloned); 14 | drake.start(item); 15 | t.plan(3); 16 | t.end(); 17 | function cloned (copy, original, type) { 18 | if (type === 'copy') { 19 | t.notEqual(copy, item, 'copy is not a reference to item'); 20 | t.equal(copy.nodeType, item.nodeType, 'copy of original is provided'); 21 | t.equal(original, item, 'original item is provided'); 22 | } 23 | } 24 | }); 25 | 26 | test('.start() emits "drag" for items', function (t) { 27 | var div = document.createElement('div'); 28 | var item = document.createElement('div'); 29 | var drake = dragula([div]); 30 | div.appendChild(item); 31 | document.body.appendChild(div); 32 | drake.on('drag', drag); 33 | drake.start(item); 34 | t.plan(2); 35 | t.end(); 36 | function drag (original, container) { 37 | t.equal(original, item, 'item is a reference to moving target'); 38 | t.equal(container, div, 'container matches expected div'); 39 | } 40 | }); 41 | 42 | test('.end() emits "cancel" when not moved', function (t) { 43 | var div = document.createElement('div'); 44 | var item = document.createElement('div'); 45 | var drake = dragula([div]); 46 | div.appendChild(item); 47 | document.body.appendChild(div); 48 | drake.on('dragend', dragend); 49 | drake.on('cancel', cancel); 50 | events.raise(item, 'mousedown', { which: 1 }); 51 | events.raise(item, 'mousemove', { which: 1 }); 52 | drake.end(); 53 | t.plan(3); 54 | t.end(); 55 | function dragend (original) { 56 | t.equal(original, item, 'item is a reference to moving target'); 57 | } 58 | function cancel (original, container) { 59 | t.equal(original, item, 'item is a reference to moving target'); 60 | t.equal(container, div, 'container matches expected div'); 61 | } 62 | }); 63 | 64 | test('.end() emits "drop" when moved', function (t) { 65 | var div = document.createElement('div'); 66 | var div2 = document.createElement('div'); 67 | var item = document.createElement('div'); 68 | var drake = dragula([div, div2]); 69 | div.appendChild(item); 70 | document.body.appendChild(div); 71 | document.body.appendChild(div2); 72 | drake.on('dragend', dragend); 73 | drake.on('drop', drop); 74 | events.raise(item, 'mousedown', { which: 1 }); 75 | events.raise(item, 'mousemove', { which: 1 }); 76 | div2.appendChild(item); 77 | drake.end(); 78 | t.plan(4); 79 | t.end(); 80 | function dragend (original) { 81 | t.equal(original, item, 'item is a reference to moving target'); 82 | } 83 | function drop (original, target, container) { 84 | t.equal(original, item, 'item is a reference to moving target'); 85 | t.equal(target, div2, 'target matches expected div'); 86 | t.equal(container, div, 'container matches expected div'); 87 | } 88 | }); 89 | 90 | test('.remove() emits "remove" for items', function (t) { 91 | var div = document.createElement('div'); 92 | var item = document.createElement('div'); 93 | var drake = dragula([div]); 94 | div.appendChild(item); 95 | document.body.appendChild(div); 96 | drake.on('dragend', dragend); 97 | drake.on('remove', remove); 98 | events.raise(item, 'mousedown', { which: 1 }); 99 | events.raise(item, 'mousemove', { which: 1 }); 100 | drake.remove(); 101 | t.plan(3); 102 | t.end(); 103 | function dragend (original) { 104 | t.equal(original, item, 'item is a reference to moving target'); 105 | } 106 | function remove (original, container) { 107 | t.equal(original, item, 'item is a reference to moving target'); 108 | t.equal(container, div, 'container matches expected div'); 109 | } 110 | }); 111 | 112 | test('.remove() emits "cancel" for copies', function (t) { 113 | var div = document.createElement('div'); 114 | var item = document.createElement('div'); 115 | var drake = dragula([div], { copy: true }); 116 | div.appendChild(item); 117 | document.body.appendChild(div); 118 | drake.on('dragend', dragend); 119 | drake.on('cancel', cancel); 120 | events.raise(item, 'mousedown', { which: 1 }); 121 | events.raise(item, 'mousemove', { which: 1 }); 122 | drake.remove(); 123 | t.plan(4); 124 | t.end(); 125 | function dragend () { 126 | t.pass('dragend got invoked'); 127 | } 128 | function cancel (copy, container) { 129 | t.notEqual(copy, item, 'copy is not a reference to item'); 130 | t.equal(copy.nodeType, item.nodeType, 'item is a copy of item'); 131 | t.equal(container, null, 'container matches expectation'); 132 | } 133 | }); 134 | 135 | test('.cancel() emits "cancel" when not moved', function (t) { 136 | var div = document.createElement('div'); 137 | var item = document.createElement('div'); 138 | var drake = dragula([div]); 139 | div.appendChild(item); 140 | document.body.appendChild(div); 141 | drake.on('dragend', dragend); 142 | drake.on('cancel', cancel); 143 | events.raise(item, 'mousedown', { which: 1 }); 144 | events.raise(item, 'mousemove', { which: 1 }); 145 | drake.cancel(); 146 | t.plan(3); 147 | t.end(); 148 | function dragend (original) { 149 | t.equal(original, item, 'item is a reference to moving target'); 150 | } 151 | function cancel (original, container) { 152 | t.equal(original, item, 'item is a reference to moving target'); 153 | t.equal(container, div, 'container matches expected div'); 154 | } 155 | }); 156 | 157 | test('.cancel() emits "drop" when not reverted', function (t) { 158 | var div = document.createElement('div'); 159 | var div2 = document.createElement('div'); 160 | var item = document.createElement('div'); 161 | var drake = dragula([div]); 162 | div.appendChild(item); 163 | document.body.appendChild(div); 164 | document.body.appendChild(div2); 165 | drake.on('dragend', dragend); 166 | drake.on('drop', drop); 167 | events.raise(item, 'mousedown', { which: 1 }); 168 | events.raise(item, 'mousemove', { which: 1 }); 169 | div2.appendChild(item); 170 | drake.cancel(); 171 | t.plan(4); 172 | t.end(); 173 | function dragend (original) { 174 | t.equal(original, item, 'item is a reference to moving target'); 175 | } 176 | function drop (original, parent, container) { 177 | t.equal(original, item, 'item is a reference to moving target'); 178 | t.equal(parent, div2, 'parent matches expected div'); 179 | t.equal(container, div, 'container matches expected div'); 180 | } 181 | }); 182 | 183 | test('.cancel() emits "cancel" when reverts', function (t) { 184 | var div = document.createElement('div'); 185 | var div2 = document.createElement('div'); 186 | var item = document.createElement('div'); 187 | var drake = dragula([div], { revertOnSpill: true }); 188 | div.appendChild(item); 189 | document.body.appendChild(div); 190 | document.body.appendChild(div2); 191 | drake.on('dragend', dragend); 192 | drake.on('cancel', cancel); 193 | events.raise(item, 'mousedown', { which: 1 }); 194 | events.raise(item, 'mousemove', { which: 1 }); 195 | div2.appendChild(item); 196 | drake.cancel(); 197 | t.plan(3); 198 | t.end(); 199 | function dragend (original) { 200 | t.equal(original, item, 'item is a reference to moving target'); 201 | } 202 | function cancel (original, container) { 203 | t.equal(original, item, 'item is a reference to moving target'); 204 | t.equal(container, div, 'container matches expected div'); 205 | } 206 | }); 207 | 208 | test('mousedown emits "cloned" for mirrors', function (t) { 209 | var div = document.createElement('div'); 210 | var item = document.createElement('div'); 211 | var drake = dragula([div]); 212 | div.appendChild(item); 213 | document.body.appendChild(div); 214 | drake.on('cloned', cloned); 215 | events.raise(item, 'mousedown', { which: 1 }); 216 | events.raise(item, 'mousemove', { which: 1 }); 217 | t.plan(3); 218 | t.end(); 219 | function cloned (copy, original, type) { 220 | if (type === 'mirror') { 221 | t.notEqual(copy, item, 'mirror is not a reference to item'); 222 | t.equal(copy.nodeType, item.nodeType, 'mirror of original is provided'); 223 | t.equal(original, item, 'original item is provided'); 224 | } 225 | } 226 | }); 227 | 228 | test('mousedown emits "cloned" for copies', function (t) { 229 | var div = document.createElement('div'); 230 | var item = document.createElement('div'); 231 | var drake = dragula([div], { copy: true }); 232 | div.appendChild(item); 233 | document.body.appendChild(div); 234 | drake.on('cloned', cloned); 235 | events.raise(item, 'mousedown', { which: 1 }); 236 | events.raise(item, 'mousemove', { which: 1 }); 237 | t.plan(3); 238 | t.end(); 239 | function cloned (copy, original, type) { 240 | if (type === 'copy') { 241 | t.notEqual(copy, item, 'copy is not a reference to item'); 242 | t.equal(copy.nodeType, item.nodeType, 'copy of original is provided'); 243 | t.equal(original, item, 'original item is provided'); 244 | } 245 | } 246 | }); 247 | 248 | test('mousedown emits "drag" for items', function (t) { 249 | var div = document.createElement('div'); 250 | var item = document.createElement('div'); 251 | var drake = dragula([div]); 252 | div.appendChild(item); 253 | document.body.appendChild(div); 254 | drake.on('drag', drag); 255 | events.raise(item, 'mousedown', { which: 1 }); 256 | events.raise(item, 'mousemove', { which: 1 }); 257 | t.plan(2); 258 | t.end(); 259 | function drag (original, container) { 260 | t.equal(original, item, 'item is a reference to moving target'); 261 | t.equal(container, div, 'container matches expected div'); 262 | } 263 | }); 264 | -------------------------------------------------------------------------------- /test/lib/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function raise (el, type, options) { 4 | var o = options || {}; 5 | var e = document.createEvent('Event'); 6 | e.initEvent(type, true, true); 7 | Object.keys(o).forEach(apply); 8 | el.dispatchEvent(e); 9 | function apply (key) { 10 | e[key] = o[key]; 11 | } 12 | } 13 | 14 | module.exports = { 15 | raise: raise 16 | }; 17 | -------------------------------------------------------------------------------- /test/public-api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var dragula = require('..'); 5 | 6 | test('public api matches expectation', function (t) { 7 | t.equal(typeof dragula, 'function', 'dragula is a function'); 8 | t.end(); 9 | }); 10 | -------------------------------------------------------------------------------- /test/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var events = require('./lib/events'); 5 | var dragula = require('..'); 6 | 7 | test('remove does not throw when not dragging', function (t) { 8 | t.test('a single time', function once (st) { 9 | var drake = dragula(); 10 | st.doesNotThrow(function () { 11 | drake.remove(); 12 | }, 'dragula ignores a single call to drake.remove'); 13 | st.end(); 14 | }); 15 | t.test('multiple times', function once (st) { 16 | var drake = dragula(); 17 | st.doesNotThrow(function () { 18 | drake.remove(); 19 | drake.remove(); 20 | drake.remove(); 21 | drake.remove(); 22 | }, 'dragula ignores multiple calls to drake.remove'); 23 | st.end(); 24 | }); 25 | t.end(); 26 | }); 27 | 28 | test('when dragging and remove gets called, element is removed', function (t) { 29 | var div = document.createElement('div'); 30 | var item = document.createElement('div'); 31 | var drake = dragula([div]); 32 | div.appendChild(item); 33 | document.body.appendChild(div); 34 | drake.start(item); 35 | drake.remove(); 36 | t.equal(div.children.length, 0, 'item got removed from container'); 37 | t.equal(drake.dragging, false, 'drake has stopped dragging'); 38 | t.end(); 39 | }); 40 | 41 | test('when dragging and remove gets called, remove event is emitted', function (t) { 42 | var div = document.createElement('div'); 43 | var item = document.createElement('div'); 44 | var drake = dragula([div]); 45 | div.appendChild(item); 46 | document.body.appendChild(div); 47 | drake.start(item); 48 | drake.on('remove', remove); 49 | drake.on('dragend', dragend); 50 | drake.remove(); 51 | t.plan(3); 52 | t.end(); 53 | function dragend () { 54 | t.pass('dragend got called'); 55 | } 56 | function remove (target, container) { 57 | t.equal(target, item, 'remove was invoked with item'); 58 | t.equal(container, div, 'remove was invoked with container'); 59 | } 60 | }); 61 | 62 | test('when dragging a copy and remove gets called, cancel event is emitted', function (t) { 63 | var div = document.createElement('div'); 64 | var item = document.createElement('div'); 65 | var drake = dragula([div], { copy: true }); 66 | div.appendChild(item); 67 | document.body.appendChild(div); 68 | events.raise(item, 'mousedown', { which: 1 }); 69 | events.raise(item, 'mousemove', { which: 1 }); 70 | drake.on('cancel', cancel); 71 | drake.on('dragend', dragend); 72 | drake.remove(); 73 | t.plan(4); 74 | t.end(); 75 | function dragend () { 76 | t.pass('dragend got called'); 77 | } 78 | function cancel (target, container) { 79 | t.equal(target.className, 'gu-transit', 'cancel was invoked with item'); 80 | t.notEqual(target, item, 'item is a copy and not the original'); 81 | t.equal(container, null, 'cancel was invoked with container'); 82 | } 83 | }); 84 | --------------------------------------------------------------------------------