├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc └── dnd-screenshot.png ├── example ├── README.md ├── basic │ ├── example.css │ ├── example.dart │ ├── images │ │ ├── README.md │ │ ├── document.png │ │ └── trash.png │ └── index.html ├── cancel │ ├── example.css │ ├── example.dart │ └── index.html ├── custom_acceptor │ ├── example.css │ ├── example.dart │ └── index.html ├── custom_avatar │ ├── example.css │ ├── example.dart │ ├── images │ │ ├── README.md │ │ ├── document.png │ │ ├── smiley01.png │ │ ├── smiley02.png │ │ ├── smiley03.png │ │ ├── smiley04.png │ │ ├── smiley05.png │ │ ├── smiley06.png │ │ └── trash.png │ └── index.html ├── detection_only │ ├── example.css │ ├── example.dart │ └── index.html ├── example.css ├── example.dart ├── free_dragging │ ├── example.css │ ├── example.dart │ └── index.html ├── handle │ ├── example.css │ ├── example.dart │ └── index.html ├── horizontal_only │ ├── example.css │ ├── example.dart │ └── index.html ├── index.html ├── nested_dropzones │ ├── example.css │ ├── example.dart │ └── index.html ├── nested_elements │ ├── example.css │ ├── example.dart │ └── index.html ├── parent_offset │ ├── example.css │ ├── example.dart │ └── index.html ├── scroll_offset │ ├── example.css │ ├── example.dart │ └── index.html └── simple_sortable │ ├── example.css │ ├── example.dart │ └── index.html ├── lib ├── dnd.dart └── src │ ├── draggable.dart │ ├── draggable_avatar.dart │ ├── draggable_dispatch.dart │ ├── draggable_manager.dart │ ├── dropzone.dart │ └── dropzone_acceptor.dart └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .packages 3 | .pub/ 4 | build/ 5 | # Remove the following pattern if you wish to check in your lock file 6 | pubspec.lock 7 | 8 | # Files generated by dart tools 9 | .dart_tool 10 | 11 | # Directory created by dartdoc 12 | doc/api/ 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 2.0.1 (2021-10-07) 4 | 5 | - Fix #36: Add null checks on `_currentDrag`. 6 | 7 | ## Version 2.0.0 (2021-03-06) 8 | 9 | - Migrate to null safety. 10 | 11 | ## Version 1.4.3 (2019-12-10) 12 | 13 | - Remove unnecessary new keyword. 14 | 15 | ## Version 1.4.2 (2019-02-13) 16 | 17 | - Fix #30: Cannot initialize Dropzone with List. 18 | - Follow more code style conventions. 19 | - Update dependencies. 20 | 21 | ## Version 1.4.0 (2018-10-12) 22 | 23 | - Add an option `minDragStartDistance` on `Draggable` to define the minimum distance in 24 | pixels that is needed for a drag to start (#26). Default is `4` pixels. This allows for clicks with tiny movement. The option `clickSuppression` has been marked as DEPRECATED as it had the same goal but a misleading name. 25 | 26 | ## Version 1.3.0 (2018-08-15) 27 | 28 | - Align dart version to support Dart 2 (#25). 29 | 30 | ## Version 1.2.0 (2018-06-21) 31 | 32 | - Support for creating a Draggable with `List` (in addition to `ElementList` and `Element`). 33 | 34 | ## Version 1.1.0 (2018-06-21) 35 | 36 | - Remove cursor files. Support for `grab`, `-webkit-grab`, `grabbing`, and `-webkit-grabbing` is good enough. 37 | 38 | ## Version 1.0.0 (2018-06-13) 39 | 40 | - Migrate to Dart 2. 41 | - Since Chrome 56 touch event listeners are [treated as passive by default](https://www.chromestatus.com/features/5093566007214080). This disables the possibility to call `preventDefault` on an element which we must do to tell the browser not to scroll while dragging. To fix this in Chrome we set the `touch-action` css property. With this Chrome will not scroll (`none`) or only scroll in one direction (`pan-y` or `pan-x`). 42 | 43 | ## Version 0.4.0 (2017-06-27) 44 | 45 | - Fix strong mode errors (#20). 46 | 47 | ## Version 0.3.6 (2017-06-06) 48 | 49 | - Fix bug: Provide reasonable fallback for event target when mouse position exits viewport (#19). 50 | 51 | ## Version 0.3.5 (2016-11-22) 52 | 53 | - Handle the edge case where destroy is called while dragging an avatar (#17). 54 | 55 | ## Version 0.3.4 (2016-10-19) 56 | 57 | - Fix strong-mode type errors (#15). 58 | - Remove Shadow DOM example and (dev)dependency on Polymer (was causing some confusion). 59 | 60 | ## Version 0.3.3 (2016-09-22) 61 | 62 | - Allow a configurable clickSuppression distance (#13). We found that the click 63 | suppression was a little too aggressive for users with less mousing accuracy. 64 | They would attempt to click and trigger a small drag. Which then suppressed the 65 | click event and prevented the action they intended to complete. 66 | 67 | ## Version 0.3.2 (2016-07-26) 68 | 69 | - Remove null-aware operator since drone.io uses an old version of Dart that doesn't support this yet. 70 | 71 | ## Version 0.3.1 (2016-07-26) 72 | 73 | - Support for programmatic drag abort (see issue #11). 74 | 75 | ## Version 0.3.0 (2015-04-18) 76 | 77 | - BREAKING CHANGE: Refactoring the `AvatarHandler`. Only if you've 78 | implemented a custom `AvatarHandler` you might need to do some changes: 79 | - `setPointerEventsNone` and `resetPointerEvents` were removed and don't 80 | need to be called any more. Pointer event styles are handled automatically. 81 | - Fix `AvatarHandler` margin caching: The `AvatarHandler` only cached the 82 | margins once for every `Draggable`. This caused problems when margins of 83 | elements in the same `Draggable` had different margins or the margins were 84 | changed. Now the margins are reset after every drag. 85 | 86 | ## Version 0.2.1 (2015-03-09) 87 | 88 | - Fix #9: Using transformers in the main `pubspec.yaml` caused problems with 89 | projects depending on the `dnd` package. 90 | 91 | ## Version 0.2.0 (2015-03-09) 92 | 93 | - Fix #3: Shadow DOM is now supported. A `dnd-retarget` attribute must be added 94 | to all custom elements where events should be forwarded to the Shadow DOM 95 | children. 96 | - Fix #7: Add a css class (`dnd-invalid` by default) to dropzones when a 97 | not-accepted draggable is dragged over. 98 | 99 | ## Version 0.1.4 (2014-10-20) 100 | 101 | - Add a sortable example. 102 | - Change comments according to new Dart Style Guide rule 103 | (`///` instead of `/** */`). 104 | - Move event dispatching calls from EventManager to Draggable (refactoring). 105 | 106 | ## Version 0.1.3 (2014-08-09) 107 | 108 | - Fix #4: Problem when an ancestor of the dragged element was positioned 109 | (relative, absolute, fixed). 110 | 111 | ## Version 0.1.2 (2014-07-22) 112 | 113 | - Correcting small bug that occurred when setSelectionRange() was called on 114 | an element that does not support it. 115 | - Fix Pointer Event bug: Too many event listeners in move, end, cancel. 116 | - Fix for Bug #1 - Not working in Windows 8.1 IE11 117 | - Adding a `cancelled` flag to `DraggableEvent` to indicate if a drag ended 118 | because of a cancelling operation like `esc` key, etc. 119 | 120 | ## Version 0.1.1 (2014-07-21) 121 | 122 | - Support for IE10 and IE11 touch screens through pointer events. 123 | - Removed `disableTouch` and `disableMouse` options. The goal was to unify 124 | touch and mouse dragging, so it should not be necessary to disable 125 | one or the other. 126 | 127 | ## Version 0.1.0 (2014-07-17) 128 | 129 | - First version. 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Marco Jakob 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dart Drag and Drop 2 | 3 | Drag and Drop for Dart web apps with mouse and touch support. 4 | 5 | [![Star this Repo](https://img.shields.io/github/stars/marcojakob/dart-dnd.svg?style=flat-square)](https://github.com/marcojakob/dart-dnd) 6 | [![Pub Package](https://img.shields.io/pub/v/dnd.svg?style=flat-square)](https://pub.dartlang.org/packages/dnd) 7 | 8 | [GitHub](https://github.com/marcojakob/dart-dnd) | 9 | [Pub](https://pub.dartlang.org/packages/dnd) | 10 | [Demos and Examples](https://code.makery.ch/library/dart-drag-and-drop/) 11 | 12 | ![DnD Screenshot](https://raw.githubusercontent.com/marcojakob/dart-dnd/master/doc/dnd-screenshot.png) 13 | 14 | ## Features 15 | 16 | - Use any HTML Element as `Draggable` or `Dropzone`. 17 | - Mouse and Touch dragging. 18 | - Draggable events: `dragStart`, `drag`, and `dragEnd` 19 | - Dropzone events: `dragEnter`, `dragOver`, `dragLeave`, and `drop` 20 | - Drag avatars as visual indication of a drag operation: 21 | - Original element as drag avatar. 22 | - Clone as drag avatar. 23 | - Custom drag avatar. 24 | - Support for Shadow DOM (Web Components, Custom Elements, Polymer, etc.). 25 | - Much more... see examples. 26 | 27 | ## Usage 28 | 29 | Before you read the instructions below, you should take a look at the 30 | [examples](https://code.makery.ch/library/dart-drag-and-drop/). 31 | 32 | ### Basic Set Up 33 | 34 | Create a `Draggable` and give it some HTML elements; this will make them 35 | draggable. You can either pass a single `Element` to the constructor or an 36 | `ElementList` that is returned by `querySelectorAll`. 37 | 38 | If you also want to drop somewhere, you'll need a `Dropzone`. 39 | 40 | ```dart 41 | // Install draggable (no avatar). 42 | Draggable draggable = Draggable(querySelectorAll('.draggable')); 43 | 44 | // Install dropzone. 45 | Dropzone dropzone = Dropzone(querySelector('.dropzone')); 46 | ``` 47 | 48 | You'll most likely want some **drag avatar** to show the user that a drag is 49 | going on. There are two predefined `AvatarHandler`s that you can use as follows. 50 | But you could also provide your own implementation of `AvatarHandler`. 51 | 52 | ```dart 53 | // Draggable with clone as avatar. 54 | Draggable draggable = Draggable(querySelectorAll('.draggable'), 55 | avatarHandler: AvatarHandler.clone()); 56 | 57 | 58 | // Draggable with original element as avatar. 59 | Draggable draggable = Draggable(querySelectorAll('.draggable'), 60 | avatarHandler: AvatarHandler.original()); 61 | ``` 62 | 63 | ### Draggable Options 64 | 65 | The following options can be passed as _named parameters_ to the constructor of 66 | `Draggable`: 67 | 68 | - `avatarHandler`: Is responsible for creating, position, and removing a drag 69 | avatar. A drag avatar provides visual feedback during a drag operation. Here 70 | are possible options (see above for an example): 71 | 72 | - `null` (the default) - will not create a drag avatar 73 | - `AvatarHandler.original()` - handler that uses the original 74 | draggable as avatar. See `OriginalAvatarHandler`. 75 | - `AvatarHandler.clone()` - handler that uses a clone of the draggable 76 | element as avatar. See `CloneAvatarHandler`. 77 | - A custom `AvatarHandler` - you can provide your own implementation of 78 | `AvatarHandler`. 79 | 80 | - `horizontalOnly`: If set to true, only horizontal dragging is tracked. 81 | This enables vertical touch dragging to be used for scrolling. 82 | 83 | - `verticalOnly`: If set to true, only vertical dragging is tracked. 84 | This enables horizontal touch dragging to be used for scrolling. 85 | 86 | - `handle`: If handle query String is specified, it restricts the dragging from 87 | starting unless it occurs on the specified element(s). Only elements that 88 | descend from the draggables elements are permitted. 89 | 90 | - `cancel`: If cancel query String is specified, drag starting is prevented on 91 | specified elements. 92 | 93 | - `draggingClass`: Is the css class set to the dragged element 94 | during a drag. If set to null, no such css class is added. 95 | 96 | - `draggingClassBody`: Is the css class set to the html body tag 97 | during a drag. If set to null, no such css class is added. 98 | 99 | - `minDragStartDistance`: Is the minimum distance in pixels that is needed 100 | for a drag to start. Default is `4`. This allows for clicks with tiny movement. 101 | 102 | ### Draggable Events 103 | 104 | Available event `Stream`s on `Draggable`: 105 | 106 | - `onDragStart`: Fired when the user starts dragging. 107 | _Note: `onDragStart` is fired not on touchStart or mouseDown but as 108 | soon as there is a drag movement. When a drag is started an `onDrag` event 109 | will also be fired._ 110 | 111 | - `onDrag`: Fired periodically throughout the drag operation. 112 | 113 | - `onDragEnd`: Fired when the user ends the dragging. 114 | _Note: Is also fired when the user clicks the 'esc'-key or the window loses focus._ 115 | 116 | ### Dropzone Options 117 | 118 | The following options can be passed as _named parameters_ to the constructor of 119 | `Dropzone`: 120 | 121 | - `acceptor`: Is used to determine which `Draggable`s will be accepted by 122 | this `Dropzone`. If none is specified, all `Draggable`s will be accepted. 123 | 124 | - `overClass`: Is the css class set to the dropzone element when an accepted 125 | draggable is dragged over it. If set to null, no such css class is added. 126 | 127 | - `invalidClass`: Is the css class set to the dropzone element when a not-accepted 128 | draggable is dragged over it. If set to null, no such css class is added. 129 | 130 | ### Dropzone Events 131 | 132 | Available event `Stream`s on `Dropzone`: 133 | 134 | - `onDragEnter`: Fired when a `Draggable` enters this `Dropzone`. 135 | 136 | - `onDragOver`: Fired periodically while a `Draggable` is moved over a `Dropzone`. 137 | 138 | - `onDragLeave`: Fired when a `Draggable` leaves this `Dropzone`. 139 | 140 | - `onDrop`: Fired when a `Draggable` is dropped inside this `Dropzone`. 141 | 142 | _Note: `Dropzone` events are only fired when the `Draggable` is accepted by 143 | the `Acceptor`._ 144 | 145 | ### Shadow DOM 146 | 147 | Web Components create a nice ecapsulation through Shadow DOM. But this creates 148 | a problem with dropzones inside the Shadow DOM as they never receive events 149 | because all events are captured by the host element. To make this work we need 150 | to retarget events to the Shadow DOM children through recursive 151 | `elementFromPoint()` calls. 152 | 153 | For performance reasons it wouldn't make sense to retarget all drag and drop 154 | events. If you wish to retarget events to the Shadow DOM children, you must add 155 | a `dnd-retarget` attribute to the host: 156 | 157 | ```dart 158 | // Retarget drag and drop events to Shadow DOM children. 159 | 160 | ``` 161 | 162 | ## Attribution 163 | 164 | The _Dart Drag and Drop_ library is inspired by 165 | 166 | - [jQuery UI Draggable](https://jqueryui.com/draggable/) 167 | - [Draggabilly](https://draggabilly.desandro.com/) 168 | 169 | Thank you for your work! 170 | 171 | ## Running / Building / Testing 172 | 173 | - Run from the terminal: `webdev serve` 174 | - Build from the terminal: `webdev build` 175 | 176 | ## License 177 | 178 | The MIT License (MIT) 179 | -------------------------------------------------------------------------------- /doc/dnd-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcojakob/dart-dnd/28d8e3fd1c6addc58d6ca37ce9c6bc2718e752aa/doc/dnd-screenshot.png -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | The root `index.html` is an overview of the examples in the subfolders. -------------------------------------------------------------------------------- /example/basic/example.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 1.428571429; 5 | color: #333; 6 | background-color: #fff; 7 | margin: 0; 8 | } 9 | 10 | .container { 11 | max-width: 400px; 12 | padding: 15px 15px 0 15px; 13 | } 14 | 15 | .trash { 16 | background: url(images/trash.png) top left no-repeat; 17 | height: 128px; 18 | width: 128px; 19 | margin-top: 90px; 20 | float: right; 21 | opacity: 0.7; 22 | } 23 | 24 | .trash.full { 25 | background: url(images/trash.png) top right no-repeat; 26 | } 27 | 28 | .document { 29 | margin-top: 10px; 30 | display: block; 31 | cursor: pointer; 32 | cursor: grab; 33 | cursor: -webkit-grab; 34 | } 35 | 36 | .dnd-over { 37 | opacity: 1; 38 | } 39 | 40 | .dnd-dragging { 41 | opacity: 0.5; 42 | } 43 | 44 | .dnd-dragging, 45 | .dnd-drag-occurring { 46 | cursor: default; 47 | cursor: grabbing; 48 | cursor: -webkit-grabbing; 49 | } 50 | -------------------------------------------------------------------------------- /example/basic/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | 3 | import 'package:dnd/dnd.dart'; 4 | 5 | /// A basic example of how to use [Draggable]s and [Dropzone]s together. 6 | main() { 7 | // Install draggables (documents). 8 | Draggable(querySelectorAll('.document'), 9 | avatarHandler: AvatarHandler.clone()); 10 | 11 | // Install dropzone (trash). 12 | Dropzone dropzone = Dropzone(querySelector('.trash')); 13 | 14 | // Remove the documents when dropped. 15 | dropzone.onDrop.listen((DropzoneEvent event) { 16 | event.draggableElement.remove(); 17 | event.dropzoneElement.classes.add('full'); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /example/basic/images/README.md: -------------------------------------------------------------------------------- 1 | # Image Sources 2 | 3 | ## Everaldo Coelho 4 | 5 | * Images: document.png, trash.png 6 | * License: GPL/LGPL 7 | * Url: http://www.everaldo.com 8 | 9 | 10 | ## LazyCrazy - Very Emotional 11 | 12 | * Images: smileys 13 | * License: Free for commercial use 14 | * Url: http://lazycrazy.deviantart.com/ -------------------------------------------------------------------------------- /example/basic/images/document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcojakob/dart-dnd/28d8e3fd1c6addc58d6ca37ce9c6bc2718e752aa/example/basic/images/document.png -------------------------------------------------------------------------------- /example/basic/images/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcojakob/dart-dnd/28d8e3fd1c6addc58d6ca37ce9c6bc2718e752aa/example/basic/images/trash.png -------------------------------------------------------------------------------- /example/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Basic Example 11 | 12 | 14 | 16 | 17 | 18 | 19 |
20 | Example Source on GitHub 22 | 23 |
24 | 26 | 28 | 30 | 32 | 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /example/cancel/example.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | line-height: 1.428571429; 10 | color: #333; 11 | background-color: #fff; 12 | margin: 0; 13 | } 14 | 15 | .container { 16 | max-width: 400px; 17 | padding: 15px 15px 0 15px; 18 | } 19 | 20 | .draggable { 21 | margin-top: 15px; 22 | padding: 10px; 23 | width: 280px; 24 | height: 210px; 25 | background: #cddc39; 26 | border-radius: 8px; 27 | cursor: pointer; 28 | cursor: grab; 29 | cursor: -webkit-grab; 30 | } 31 | 32 | .no-drag { 33 | padding: 10px; 34 | margin-top: 20px; 35 | height: 20px; 36 | border-radius: 8px; 37 | background-color: #afb42b; 38 | cursor: default; 39 | } 40 | 41 | textarea { 42 | margin-top: 15px; 43 | width: 270px; 44 | height: 50px; 45 | resize: none; 46 | } 47 | 48 | button { 49 | margin-top: 15px; 50 | height: 30px; 51 | } 52 | 53 | .dnd-draggable, 54 | .dnd-drag-occurring { 55 | cursor: default; 56 | cursor: grabbing; 57 | cursor: -webkit-grabbing; 58 | } 59 | 60 | .dnd-dragging { 61 | opacity: 0.5; 62 | } 63 | -------------------------------------------------------------------------------- /example/cancel/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:dnd/dnd.dart'; 3 | 4 | /// Example demonstrating how dragging can be prevented on some elements. 5 | main() { 6 | // Install draggable. 7 | Draggable(querySelector('.draggable'), 8 | avatarHandler: AvatarHandler.original(), 9 | cancel: 'textarea, button, .no-drag'); 10 | } 11 | -------------------------------------------------------------------------------- /example/cancel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Cancel Example 11 | 12 | 14 | 16 | 17 | 18 | 19 |
20 | Example Source on GitHub 22 | 23 |
24 |
Drag me!
25 |
No drag here!
26 | 27 | 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /example/custom_acceptor/example.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | line-height: 1.428571429; 10 | color: #333; 11 | background-color: #fff; 12 | margin: 0; 13 | } 14 | 15 | .container { 16 | max-width: 500px; 17 | padding: 15px 15px 0 15px; 18 | } 19 | 20 | .draggable { 21 | margin-top: 15px; 22 | width: 120px; 23 | height: 85px; 24 | background: #cddc39; 25 | border-radius: 8px; 26 | text-align: center; 27 | cursor: pointer; 28 | cursor: grab; 29 | cursor: -webkit-grab; 30 | } 31 | 32 | .draggable p { 33 | padding-top: 10px; 34 | margin: 0; 35 | font-weight: bold; 36 | } 37 | 38 | .draggable input { 39 | width: 90px; 40 | } 41 | 42 | .dropzone-container { 43 | float: right; 44 | } 45 | 46 | .dropzone { 47 | margin-top: 15px; 48 | width: 130px; 49 | height: 120px; 50 | background: #ffc107; 51 | border-radius: 8px; 52 | text-align: center; 53 | } 54 | 55 | .dropzone p { 56 | margin: 0; 57 | padding-top: 10px; 58 | font-weight: bold; 59 | } 60 | 61 | .dnd-dragging { 62 | opacity: 0.7; 63 | } 64 | 65 | .dnd-over { 66 | background-color: #ffe607; 67 | } 68 | 69 | .dnd-invalid { 70 | background-color: grey; 71 | cursor: not-allowed; 72 | } 73 | 74 | .dnd-dragging, 75 | .dnd-drag-occurring { 76 | cursor: default; 77 | cursor: grabbing; 78 | cursor: -webkit-grabbing; 79 | } 80 | -------------------------------------------------------------------------------- /example/custom_acceptor/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:dnd/dnd.dart'; 3 | 4 | /// Uses Acceptors to determine which Draggables are accepted by which Dropzones. 5 | main() { 6 | // Install draggables. 7 | Draggable(querySelector('#draggable-a'), 8 | avatarHandler: AvatarHandler.clone()); 9 | var draggableB = Draggable(querySelector('#draggable-b'), 10 | avatarHandler: AvatarHandler.clone()); 11 | Draggable(querySelector('#draggable-c'), 12 | avatarHandler: AvatarHandler.clone()); 13 | 14 | // No acceptor means everything is accepted. 15 | Dropzone(querySelector('#dropzone-1')); 16 | 17 | // Use provided DraggablesAcceptor to accept Draggable B only. 18 | Dropzone(querySelector('#dropzone-2'), 19 | acceptor: Acceptor.draggables([draggableB])); 20 | 21 | // Use a custom Acceptor that accepts Draggables with a input containing 22 | // the text 'acceptme'. 23 | Dropzone(querySelector('#dropzone-3'), acceptor: MyAcceptor()); 24 | } 25 | 26 | /** 27 | * Custom acceptor that accepts [Draggable]s with an input containing 28 | * the text 'acceptme'. 29 | */ 30 | class MyAcceptor extends Acceptor { 31 | @override 32 | bool accepts( 33 | Element draggableElement, int draggableId, Element dropzoneElement) { 34 | InputElement? input = 35 | draggableElement.querySelector('input') as InputElement?; 36 | return input != null && input.value == 'acceptme'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/custom_acceptor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Custom Acceptor Example 11 | 12 | 14 | 16 | 17 | 18 | 19 |
20 | Example Source on GitHub 22 | 23 |
24 |
26 |

Dropzone 1

27 | (Accepts all) 28 |
29 |
31 |

Dropzone 2

32 | (Accepts B only) 33 |
34 |
36 |

Dropzone 3

37 | (Accepts if textfield contains word 'acceptme') 38 |
39 |
40 | 41 |
43 |

Draggable A

44 |
45 |
47 |

Draggable B

48 |
49 |
51 |

Draggable C

52 | Enter 'acceptme': 53 |
54 | 55 |
56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /example/custom_avatar/example.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 1.428571429; 5 | color: #333; 6 | background-color: #fff; 7 | margin: 0; 8 | } 9 | 10 | .container { 11 | max-width: 500px; 12 | padding: 15px 15px 0 15px; 13 | } 14 | 15 | .trash { 16 | background: url(images/trash.png) top left no-repeat; 17 | height: 128px; 18 | width: 128px; 19 | margin-top: 90px; 20 | float: right; 21 | opacity: 0.7; 22 | } 23 | 24 | .trash.full { 25 | background: url(images/trash.png) top right no-repeat; 26 | } 27 | 28 | .document { 29 | margin-top: 10px; 30 | display: block; 31 | cursor: pointer; 32 | cursor: grab; 33 | cursor: -webkit-grab; 34 | } 35 | 36 | .dnd-over { 37 | opacity: 1; 38 | } 39 | 40 | .dnd-dragging { 41 | opacity: 0.5; 42 | } 43 | 44 | .dnd-dragging, 45 | .dnd-drag-occurring { 46 | cursor: default; 47 | cursor: grabbing; 48 | cursor: -webkit-grabbing; 49 | } 50 | -------------------------------------------------------------------------------- /example/custom_avatar/example.dart: -------------------------------------------------------------------------------- 1 | library dnd_example; 2 | 3 | import 'dart:html'; 4 | import 'dart:math' as math; 5 | import 'package:dnd/dnd.dart'; 6 | 7 | /// An example of how to use a custom [AvatarHandler]. 8 | /// 9 | /// See [MyAvatarHandler]. 10 | main() { 11 | // Install draggables. 12 | Draggable draggable = Draggable(querySelectorAll('.document'), 13 | avatarHandler: MyAvatarHandler()); 14 | 15 | // Install dropzone (trash). 16 | Element trash = querySelector('.trash') as Element; 17 | Dropzone dropzone = Dropzone(trash); 18 | 19 | // Keep track if we're over the trash. 20 | bool overTrash = false; 21 | dropzone.onDragEnter.listen((DropzoneEvent event) { 22 | overTrash = true; 23 | }); 24 | dropzone.onDragLeave.listen((DropzoneEvent event) { 25 | overTrash = false; 26 | }); 27 | 28 | // Change the drag avatar during the drag. 29 | draggable.onDrag.listen((DraggableEvent event) { 30 | MyAvatarHandler handler = event.avatarHandler as MyAvatarHandler; 31 | 32 | if (overTrash) { 33 | // Set to last image if over trash. 34 | handler.updateImage(4); 35 | } else { 36 | // Set image depending on distance to trash. 37 | int imageNumber = 38 | calcImageNumber(trash, event.startPosition, event.position); 39 | handler.updateImage(imageNumber); 40 | } 41 | }); 42 | 43 | // Remove the documents when dropped. 44 | dropzone.onDrop.listen((DropzoneEvent event) { 45 | event.draggableElement.remove(); 46 | event.dropzoneElement.classes.add('full'); 47 | }); 48 | } 49 | 50 | /// An example of a custom [AvatarHandler]. 51 | /// 52 | /// The [MyAvatarHandler] creates changing smiley images as drag avatar. 53 | class MyAvatarHandler extends AvatarHandler { 54 | /// List of smiley src urls. 55 | static final List SMILEYS = [ 56 | 'images/smiley02.png', 57 | 'images/smiley03.png', 58 | 'images/smiley04.png', 59 | 'images/smiley05.png', 60 | 'images/smiley06.png' 61 | ]; 62 | 63 | /// Define an offset for the avatar relative to the mouse cursor. 64 | static const Point OFFSET = const Point(-64, -130); 65 | 66 | MyAvatarHandler() { 67 | // Preload avatar images. 68 | SMILEYS.forEach((s) { 69 | ImageElement(src: s); 70 | }); 71 | } 72 | 73 | @override 74 | void dragStart(Element draggable, Point startPosition) { 75 | // Use first image as avatar. 76 | avatar = ImageElement(src: SMILEYS[0]); 77 | 78 | // Set the initial position of avatar. 79 | setLeftTop(startPosition + OFFSET); 80 | 81 | // Ensure avatar has an absolute position. 82 | avatar!.style.position = 'absolute'; 83 | 84 | // Add the drag avatar to the body element. 85 | document.body!.append(avatar!); 86 | } 87 | 88 | @override 89 | void drag(Point startPosition, Point position) { 90 | setTranslate(position - startPosition); 91 | } 92 | 93 | @override 94 | void dragEnd(Point startPosition, Point position) { 95 | if (avatar != null) { 96 | avatar!.remove(); 97 | } 98 | } 99 | 100 | /// Updates the image to [imageNumber]. 101 | void updateImage(int imageNumber) { 102 | (avatar as ImageElement).src = SMILEYS[imageNumber]; 103 | } 104 | } 105 | 106 | /// Calculates the image number depending on the distance to [trash]. 107 | int calcImageNumber(Element trash, Point startPosition, Point position) { 108 | var trashDim = trash.borderEdge; 109 | Point trashCenter = Point( 110 | trashDim.left + trashDim.width / 2, trashDim.top + trashDim.height / 2); 111 | 112 | // Set image depending on distance to trash. 113 | double remainingDistance = position.distanceTo(trashCenter) - 64; 114 | double totalDistance = startPosition.distanceTo(trashCenter) - 64; 115 | 116 | num distancePercent = remainingDistance / totalDistance; 117 | distancePercent = math.min(1, distancePercent); 118 | 119 | return 3 - (3 * distancePercent).round(); 120 | } 121 | -------------------------------------------------------------------------------- /example/custom_avatar/images/README.md: -------------------------------------------------------------------------------- 1 | # Image Sources 2 | 3 | ## Everaldo Coelho 4 | 5 | * Images: document.png, trash.png 6 | * License: GPL/LGPL 7 | * Url: http://www.everaldo.com 8 | 9 | 10 | ## LazyCrazy - Very Emotional 11 | 12 | * Images: smileys 13 | * License: Free for commercial use 14 | * Url: http://lazycrazy.deviantart.com/ -------------------------------------------------------------------------------- /example/custom_avatar/images/document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcojakob/dart-dnd/28d8e3fd1c6addc58d6ca37ce9c6bc2718e752aa/example/custom_avatar/images/document.png -------------------------------------------------------------------------------- /example/custom_avatar/images/smiley01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcojakob/dart-dnd/28d8e3fd1c6addc58d6ca37ce9c6bc2718e752aa/example/custom_avatar/images/smiley01.png -------------------------------------------------------------------------------- /example/custom_avatar/images/smiley02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcojakob/dart-dnd/28d8e3fd1c6addc58d6ca37ce9c6bc2718e752aa/example/custom_avatar/images/smiley02.png -------------------------------------------------------------------------------- /example/custom_avatar/images/smiley03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcojakob/dart-dnd/28d8e3fd1c6addc58d6ca37ce9c6bc2718e752aa/example/custom_avatar/images/smiley03.png -------------------------------------------------------------------------------- /example/custom_avatar/images/smiley04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcojakob/dart-dnd/28d8e3fd1c6addc58d6ca37ce9c6bc2718e752aa/example/custom_avatar/images/smiley04.png -------------------------------------------------------------------------------- /example/custom_avatar/images/smiley05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcojakob/dart-dnd/28d8e3fd1c6addc58d6ca37ce9c6bc2718e752aa/example/custom_avatar/images/smiley05.png -------------------------------------------------------------------------------- /example/custom_avatar/images/smiley06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcojakob/dart-dnd/28d8e3fd1c6addc58d6ca37ce9c6bc2718e752aa/example/custom_avatar/images/smiley06.png -------------------------------------------------------------------------------- /example/custom_avatar/images/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcojakob/dart-dnd/28d8e3fd1c6addc58d6ca37ce9c6bc2718e752aa/example/custom_avatar/images/trash.png -------------------------------------------------------------------------------- /example/custom_avatar/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Custom Avatar Example 11 | 12 | 14 | 16 | 17 | 18 | 19 | 20 |
21 | Example Source on GitHub 23 | 24 |
25 | 27 | 29 | 31 | 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /example/detection_only/example.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | line-height: 1.428571429; 10 | color: #333; 11 | background-color: #fff; 12 | margin: 0; 13 | } 14 | 15 | .container { 16 | max-width: 400px; 17 | padding: 15px 15px 0 15px; 18 | } 19 | 20 | .draggable { 21 | margin-top: 15px; 22 | width: 120px; 23 | height: 120px; 24 | background: #cddc39; 25 | border-radius: 8px; 26 | cursor: pointer; 27 | cursor: grab; 28 | cursor: -webkit-grab; 29 | } 30 | 31 | .draggable p { 32 | text-align: center; 33 | padding-top: 30px; 34 | font-weight: bold; 35 | } 36 | 37 | .dnd-dragging { 38 | opacity: 0.7; 39 | } 40 | 41 | .dnd-dragging, 42 | .dnd-drag-occurring { 43 | cursor: default; 44 | cursor: grabbing; 45 | cursor: -webkit-grabbing; 46 | } 47 | -------------------------------------------------------------------------------- /example/detection_only/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:dnd/dnd.dart'; 3 | 4 | /// An example that only uses drag detection. This already helps quite a lot 5 | /// as it unifies touch and mouse dragging and provides convenient event streams. 6 | /// 7 | /// Use this if you want to implement your own custom dragging behavior. 8 | main() { 9 | // Install draggable. 10 | Draggable draggable = Draggable(querySelector('.draggable')); 11 | 12 | // Paragraph. 13 | Element p = querySelector('.draggable p') as Element; 14 | 15 | // Listen to drag start. 16 | draggable.onDragStart.listen((DraggableEvent event) { 17 | p.innerHtml = 'DragStart:
${round(event.position)}'; 18 | }); 19 | 20 | // Listen do drag. 21 | draggable.onDrag.listen((DraggableEvent event) { 22 | p.innerHtml = 'Drag:
${round(event.position)}'; 23 | }); 24 | 25 | // Listen to dragEnd. 26 | draggable.onDragEnd.listen((DraggableEvent event) { 27 | p.innerHtml = 'DragEnd: ${round(event.position)}'; 28 | }); 29 | } 30 | 31 | Point round(Point point) { 32 | return Point(point.x.round(), point.y.round()); 33 | } 34 | -------------------------------------------------------------------------------- /example/detection_only/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Detection Only Example 11 | 12 | 14 | 16 | 17 | 18 | 19 |
20 | Example Source on GitHub 22 | 23 |
24 |

Drag me!

25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /example/example.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 7 | font-size: 14px; 8 | line-height: 1.428571429; 9 | color: #333; 10 | background-color: #fff; 11 | margin: 0; 12 | } 13 | 14 | .container { 15 | max-width: 500px; 16 | padding: 15px; 17 | } -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | // Only used as placeholder. -------------------------------------------------------------------------------- /example/free_dragging/example.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | line-height: 1.428571429; 10 | color: #333; 11 | background-color: #fff; 12 | margin: 0; 13 | } 14 | 15 | .container { 16 | max-width: 400px; 17 | padding: 15px 15px 0 15px; 18 | } 19 | 20 | .draggable { 21 | margin-top: 15px; 22 | width: 120px; 23 | height: 120px; 24 | background: #cddc39; 25 | border-radius: 8px; 26 | cursor: pointer; 27 | cursor: grab; 28 | cursor: -webkit-grab; 29 | } 30 | 31 | .draggable p { 32 | text-align: center; 33 | padding-top: 30px; 34 | margin: 0; 35 | font-weight: bold; 36 | } 37 | 38 | .dnd-dragging { 39 | opacity: 0.5; 40 | } 41 | 42 | .dnd-dragging, 43 | .dnd-drag-occurring { 44 | cursor: default; 45 | cursor: grabbing; 46 | cursor: -webkit-grabbing; 47 | } 48 | -------------------------------------------------------------------------------- /example/free_dragging/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:dnd/dnd.dart'; 3 | 4 | /// An example of a how to freely drag a [Draggable]. Instead of using a clone 5 | /// for the drag avatar, the original element itself is dragged. 6 | main() { 7 | // Install draggable. 8 | Draggable(querySelector('.draggable'), 9 | avatarHandler: AvatarHandler.original()); 10 | } 11 | -------------------------------------------------------------------------------- /example/free_dragging/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Free Dragging Example 11 | 12 | 14 | 16 | 17 | 18 | 19 |
20 | Example Source on GitHub 22 | 23 |
24 |

Drag me!

25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /example/handle/example.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | line-height: 1.428571429; 10 | color: #333; 11 | background-color: #fff; 12 | margin: 0; 13 | } 14 | 15 | .container { 16 | max-width: 400px; 17 | padding: 15px 15px 0 15px; 18 | } 19 | 20 | .draggable { 21 | margin-top: 15px; 22 | width: 180px; 23 | height: 120px; 24 | background: #cddc39; 25 | border-radius: 8px; 26 | } 27 | 28 | .handle { 29 | position: relative; 30 | width: 50px; 31 | height: 50px; 32 | left: 10px; 33 | top: 35px; 34 | border-radius: 8px; 35 | background-color: #afb42b; 36 | cursor: pointer; 37 | cursor: grab; 38 | cursor: -webkit-grab; 39 | } 40 | 41 | .dnd-draggable, 42 | .dnd-drag-occurring { 43 | cursor: default; 44 | cursor: grabbing; 45 | cursor: -webkit-grabbing; 46 | } 47 | 48 | .dnd-dragging { 49 | opacity: 0.5; 50 | } 51 | -------------------------------------------------------------------------------- /example/handle/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:dnd/dnd.dart'; 3 | 4 | /// Example that uses a sub-element as drag handle. 5 | main() { 6 | // Install draggable. 7 | Draggable(querySelector('.draggable'), 8 | avatarHandler: AvatarHandler.original(), handle: '.handle'); 9 | } 10 | -------------------------------------------------------------------------------- /example/handle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Handle Example 11 | 12 | 14 | 16 | 17 | 18 | 19 |
20 | Example Source on GitHub 22 | 23 |
24 |
25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /example/horizontal_only/example.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | line-height: 1.428571429; 10 | color: #333; 11 | background-color: #fff; 12 | margin: 0; 13 | } 14 | 15 | .container { 16 | max-width: 400px; 17 | padding: 15px 15px 0 15px; 18 | } 19 | 20 | .draggable { 21 | margin-top: 15px; 22 | width: 120px; 23 | height: 120px; 24 | background: #cddc39; 25 | border-radius: 8px; 26 | cursor: pointer; 27 | cursor: grab; 28 | cursor: -webkit-grab; 29 | } 30 | 31 | .draggable p { 32 | text-align: center; 33 | padding-top: 30px; 34 | margin: 0; 35 | font-weight: bold; 36 | } 37 | 38 | .dnd-dragging { 39 | opacity: 0.5; 40 | } 41 | 42 | .dnd-dragging, 43 | .dnd-drag-occurring { 44 | cursor: default; 45 | cursor: grabbing; 46 | cursor: -webkit-grabbing; 47 | } 48 | -------------------------------------------------------------------------------- /example/horizontal_only/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:dnd/dnd.dart'; 3 | 4 | /// An example of a how to restrict dragging to the horizontal axis. 5 | /// A nice side effect: When a touch drag is started in the vertical direction 6 | /// it is ignored and thus can be used for scrolling on touch devices. 7 | main() { 8 | // Install draggable. 9 | Draggable(querySelector('.draggable'), 10 | avatarHandler: AvatarHandler.original(), horizontalOnly: true); 11 | } 12 | -------------------------------------------------------------------------------- /example/horizontal_only/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Horizontal Only Example 11 | 12 | 14 | 16 | 17 | 18 | 19 |
20 | Example Source on GitHub 22 | 23 |
24 |

Drag me horizontally!

25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 12 | 13 | Dart Drag and Drop Examples 14 | 15 | 16 | 17 |
18 |

Dart Drag and Drop Examples

19 |

20 | Here are some examples of how to use Dart Drag and Drop: 21 |

22 | 57 | 58 |

Just for Testing

59 |

60 | The following examples are used for testing purposes: 61 |

62 | 70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /example/nested_dropzones/example.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | line-height: 1.428571429; 10 | color: #333; 11 | background-color: #fff; 12 | margin: 0; 13 | } 14 | 15 | .container { 16 | max-width: 500px; 17 | padding: 15px 15px 0 15px; 18 | } 19 | 20 | .draggable { 21 | margin-top: 15px; 22 | position: absolute; 23 | z-index: 100; 24 | width: 120px; 25 | height: 50px; 26 | background: #cddc39; 27 | border-radius: 8px; 28 | cursor: pointer; 29 | cursor: grab; 30 | cursor: -webkit-grab; 31 | } 32 | 33 | .draggable p { 34 | text-align: center; 35 | font-weight: bold; 36 | } 37 | 38 | .dropzone-outer { 39 | position: relative; 40 | top: 60px; 41 | padding: 10px; 42 | margin-top: 20px; 43 | width: 280px; 44 | height: 180px; 45 | background: #ffc107; 46 | border-radius: 8px; 47 | } 48 | 49 | .dropzone-inner { 50 | padding: 10px; 51 | margin-top: 40px; 52 | height: 40px; 53 | border-radius: 8px; 54 | background-color: #ffe082; 55 | } 56 | 57 | .dnd-draggable, 58 | .dnd-drag-occurring { 59 | cursor: default; 60 | cursor: grabbing; 61 | cursor: -webkit-grabbing; 62 | } 63 | 64 | .dnd-dragging { 65 | opacity: 0.7; 66 | } 67 | 68 | .dnd-over { 69 | background-color: #ffe607; 70 | } 71 | -------------------------------------------------------------------------------- /example/nested_dropzones/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:dnd/dnd.dart'; 3 | 4 | /// Example how to drag over nested [Dropzone]s. 5 | /// 6 | /// Note: If dropped on an inner [Dropzone] the outer [Dropzone] will also 7 | /// receive the drop event. 8 | main() { 9 | // Install draggable. 10 | Draggable draggable = Draggable(querySelector('.draggable'), 11 | avatarHandler: AvatarHandler.original()); 12 | 13 | // Install dropzones. 14 | Dropzone outerDropzone = Dropzone(querySelector('.dropzone-outer')); 15 | Dropzone innerDropzone = Dropzone(querySelector('.dropzone-inner')); 16 | 17 | // The text elements. 18 | Element draggableText = querySelector('.draggable > p') as Element; 19 | Element outerText = querySelector('.dropzone-outer > span') as Element; 20 | Element innerText = querySelector('.dropzone-inner > span') as Element; 21 | 22 | // Dropped flags to help for displaying the correct text in dragLeave 23 | // (dragLeave is fired after a drop). 24 | bool outerDropped = false; 25 | bool innerDropped = false; 26 | 27 | // Listen to dragEnter. 28 | outerDropzone.onDragEnter.listen((DropzoneEvent event) { 29 | outerText.text = 'Outer Dropzone: Enter'; 30 | }); 31 | innerDropzone.onDragEnter.listen((DropzoneEvent event) { 32 | innerText.text = 'Inner Dropzone: Enter'; 33 | }); 34 | 35 | // Listen to dragLeave. 36 | outerDropzone.onDragLeave.listen((DropzoneEvent event) { 37 | if (outerDropped) { 38 | outerText.text = 'Outer Dropzone: Drop, Leave'; 39 | } else { 40 | outerText.text = 'Outer Dropzone: Leave'; 41 | } 42 | }); 43 | innerDropzone.onDragLeave.listen((DropzoneEvent event) { 44 | if (innerDropped) { 45 | innerText.text = 'Inner Dropzone: Drop, Leave'; 46 | } else { 47 | innerText.text = 'Inner Dropzone: Leave'; 48 | } 49 | }); 50 | 51 | // Listen to drop. 52 | outerDropzone.onDrop.listen((DropzoneEvent event) { 53 | outerDropped = true; 54 | }); 55 | innerDropzone.onDrop.listen((DropzoneEvent event) { 56 | innerDropped = true; 57 | }); 58 | 59 | // Listen to dragStart to reset. 60 | draggable.onDragStart.listen((DraggableEvent event) { 61 | outerDropped = false; 62 | innerDropped = false; 63 | draggableText.text = 'Drag me!'; 64 | outerText.text = 'Outer Dropzone'; 65 | innerText.text = 'Inner Dropzone'; 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /example/nested_dropzones/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Nested Dropzones Example 11 | 12 | 14 | 16 | 17 | 18 | 19 |
20 | Example Source on GitHub 22 | 23 |
24 |

Drag me!

25 |
26 | 27 |
28 | Outer Dropzone 29 | 30 |
31 | Inner Dropzone 32 |
33 |
34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /example/nested_elements/example.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | line-height: 1.428571429; 10 | color: #333; 11 | background-color: #fff; 12 | margin: 0; 13 | } 14 | 15 | .container { 16 | max-width: 500px; 17 | padding: 15px 15px 0 15px; 18 | } 19 | 20 | .draggable { 21 | margin-top: 15px; 22 | position: absolute; 23 | z-index: 100; 24 | width: 120px; 25 | height: 50px; 26 | background: #cddc39; 27 | border-radius: 8px; 28 | cursor: pointer; 29 | cursor: grab; 30 | cursor: -webkit-grab; 31 | } 32 | 33 | .draggable p { 34 | text-align: center; 35 | font-weight: bold; 36 | } 37 | 38 | .dropzone { 39 | position: relative; 40 | top: 60px; 41 | padding: 10px; 42 | margin-top: 20px; 43 | width: 280px; 44 | height: 200px; 45 | background: #ffc107; 46 | border-radius: 8px; 47 | } 48 | 49 | .inner { 50 | padding: 10px; 51 | margin-top: 10px; 52 | height: 20px; 53 | border-radius: 8px; 54 | background-color: #ffe082; 55 | cursor: default; 56 | } 57 | 58 | textarea { 59 | margin-top: 15px; 60 | width: 270px; 61 | height: 50px; 62 | resize: none; 63 | } 64 | 65 | button { 66 | margin-top: 15px; 67 | height: 30px; 68 | } 69 | 70 | .dnd-draggable, 71 | .dnd-drag-occurring { 72 | cursor: default; 73 | cursor: grabbing; 74 | cursor: -webkit-grabbing; 75 | } 76 | 77 | .dnd-dragging { 78 | opacity: 0.7; 79 | } 80 | 81 | .dnd-over { 82 | background-color: #ffe607; 83 | } 84 | -------------------------------------------------------------------------------- /example/nested_elements/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:dnd/dnd.dart'; 3 | 4 | /// Example demonstrating how to drag over nested elements and get correct 5 | /// dragEnter and dragLeave events. 6 | main() { 7 | // Install draggable. 8 | Draggable(querySelector('.draggable'), 9 | avatarHandler: AvatarHandler.original()); 10 | 11 | // Install dropzone. 12 | Dropzone dropzone = Dropzone(querySelector('.dropzone')); 13 | 14 | // Text element 15 | Element text = querySelector('.dropzone > span') as Element; 16 | 17 | // Listen to dragEnter. 18 | dropzone.onDragEnter.listen((DropzoneEvent event) { 19 | text.text = 'Outer div: enter'; 20 | }); 21 | 22 | // Listen to dragLeave. 23 | dropzone.onDragLeave.listen((DropzoneEvent event) { 24 | text.text = 'Outer div: leave'; 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /example/nested_elements/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Nested Elements Example 11 | 12 | 14 | 16 | 17 | 18 | 19 |
20 | Example Source on GitHub 22 | 23 |
24 |

Drag me!

25 |
26 | 27 |
28 | Outer div 29 |
Inner div
30 | 31 | 32 |
33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /example/parent_offset/example.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | line-height: 1.428571429; 10 | color: #333; 11 | background-color: #fff; 12 | margin: 0; 13 | } 14 | 15 | .container { 16 | max-width: 500px; 17 | padding: 15px 15px 0 15px; 18 | position: absolute; 19 | top: 200px; 20 | left: 100px; 21 | background: #ffc107; 22 | } 23 | 24 | .draggable { 25 | margin: 70px; 26 | width: 180px; 27 | height: 120px; 28 | background: #cddc39; 29 | border-radius: 8px; 30 | cursor: pointer; 31 | cursor: grab; 32 | cursor: -webkit-grab; 33 | } 34 | 35 | .draggable p { 36 | text-align: center; 37 | padding-top: 30px; 38 | margin: 0; 39 | font-weight: bold; 40 | } 41 | 42 | .dnd-dragging { 43 | opacity: 0.5; 44 | } 45 | 46 | .dnd-dragging, 47 | .dnd-drag-occurring { 48 | cursor: default; 49 | cursor: grabbing; 50 | cursor: -webkit-grabbing; 51 | } 52 | -------------------------------------------------------------------------------- /example/parent_offset/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:dnd/dnd.dart'; 3 | 4 | /// A test with a draggable that has a parent with offset > 0. 5 | main() { 6 | // Install draggable. 7 | Draggable(querySelector('.draggable'), avatarHandler: AvatarHandler.clone()); 8 | } 9 | -------------------------------------------------------------------------------- /example/parent_offset/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Parent Offset Test 11 | 12 | 14 | 16 | 17 | 18 | 19 |
20 | Example Source on GitHub 22 |
23 |

Drag me!

24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /example/scroll_offset/example.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | line-height: 1.428571429; 10 | color: #333; 11 | background-color: #fff; 12 | margin: 0; 13 | } 14 | 15 | .container { 16 | padding: 15px 15px 0 15px; 17 | height: 400px; 18 | overflow: scroll; 19 | /* For scrolling we could use position relative to adjust to the scroll. 20 | position: relative; 21 | */ 22 | } 23 | 24 | .draggable { 25 | margin-top: 200px; 26 | width: 180px; 27 | height: 120px; 28 | background: #cddc39; 29 | border-radius: 8px; 30 | cursor: pointer; 31 | cursor: grab; 32 | cursor: -webkit-grab; 33 | } 34 | 35 | .draggable p { 36 | text-align: center; 37 | padding-top: 30px; 38 | margin: 0; 39 | font-weight: bold; 40 | } 41 | 42 | .dropzone { 43 | margin-top: 20px; 44 | padding: 10px; 45 | width: 280px; 46 | height: 180px; 47 | background: #ffc107; 48 | border-radius: 8px; 49 | } 50 | 51 | .dnd-dragging { 52 | opacity: 0.5; 53 | } 54 | 55 | .dnd-dragging, 56 | .dnd-drag-occurring { 57 | cursor: default; 58 | cursor: grabbing; 59 | cursor: -webkit-grabbing; 60 | } 61 | 62 | .dnd-over { 63 | background-color: #ffe607; 64 | } 65 | -------------------------------------------------------------------------------- /example/scroll_offset/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:dnd/dnd.dart'; 3 | 4 | /// A test with a draggable that has a scrollable parent. 5 | main() { 6 | // Install draggable. 7 | Draggable(querySelector('.draggable'), avatarHandler: AvatarHandler.clone()); 8 | 9 | // Install dropzone. 10 | Dropzone(querySelector('.dropzone')); 11 | } 12 | -------------------------------------------------------------------------------- /example/scroll_offset/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Scroll Offset Test 11 | 12 | 14 | 16 | 17 | 18 | 19 |
20 | Example Source on GitHub 22 |
23 |

Drag me!

24 |
25 | 26 |
27 | Scroll down and drag! 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /example/simple_sortable/example.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | line-height: 1.428571429; 10 | color: #333; 11 | background-color: #fff; 12 | margin: 0; 13 | } 14 | 15 | .container { 16 | max-width: 400px; 17 | padding: 15px 15px 0 15px; 18 | } 19 | 20 | .sortable { 21 | margin-top: 15px; 22 | width: 240px; 23 | height: 120px; 24 | background: #cddc39; 25 | border-radius: 8px; 26 | box-sizing: border-box; 27 | cursor: pointer; 28 | cursor: grab; 29 | cursor: -webkit-grab; 30 | } 31 | 32 | .sortable p { 33 | text-align: center; 34 | padding-top: 30px; 35 | margin: 0; 36 | font-weight: bold; 37 | } 38 | 39 | .dnd-dragging { 40 | opacity: 0.5; 41 | } 42 | 43 | .dnd-dragging, 44 | .dnd-drag-occurring { 45 | cursor: default; 46 | cursor: grabbing; 47 | cursor: -webkit-grabbing; 48 | } 49 | 50 | .dnd-over { 51 | border-width: medium; 52 | border-color: #cddc39; 53 | border-style: dashed; 54 | background-color: white; 55 | opacity: 1; 56 | } 57 | 58 | .dnd-over * { 59 | display: none; 60 | } 61 | -------------------------------------------------------------------------------- /example/simple_sortable/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:dnd/dnd.dart'; 3 | 4 | /// Just a test if we can set up [Draggable] and [Dropzone] on the same elements 5 | /// and implement some basic sortable/rearranging behavior. 6 | main() { 7 | // Install same elements as draggable and dropzone. 8 | Draggable(querySelectorAll('.sortable'), 9 | avatarHandler: AvatarHandler.clone()); 10 | 11 | Dropzone dropzone = Dropzone(querySelectorAll('.sortable')); 12 | 13 | // Swap elements when dropped. 14 | dropzone.onDrop.listen((DropzoneEvent event) { 15 | swapElements(event.draggableElement, event.dropzoneElement); 16 | }); 17 | } 18 | 19 | /// Simple function to swap two elements. 20 | void swapElements(Element elm1, Element elm2) { 21 | var parent1 = elm1.parent as Element; 22 | var next1 = elm1.nextElementSibling; 23 | var parent2 = elm2.parent as Element; 24 | var next2 = elm2.nextElementSibling; 25 | 26 | parent1.insertBefore(elm2, next1); 27 | parent2.insertBefore(elm1, next2); 28 | } 29 | -------------------------------------------------------------------------------- /example/simple_sortable/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | Dart Drag and Drop: Sortable Test 11 | 12 | 14 | 16 | 17 | 18 | 19 |
20 | 21 | Example Source on GitHub 23 | 24 |
25 |
26 |

Drag me! 27 |
28 |
A

29 |
30 |
31 |

Drag me! 32 |
33 |
B

34 |
35 |
36 |

Drag me! 37 |
38 |
C

39 |
40 |
41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /lib/dnd.dart: -------------------------------------------------------------------------------- 1 | library dnd; 2 | 3 | import 'dart:html'; 4 | import 'dart:async'; 5 | import 'dart:math' as math; 6 | import 'dart:js'; 7 | 8 | part 'src/draggable.dart'; 9 | part 'src/draggable_avatar.dart'; 10 | part 'src/draggable_dispatch.dart'; 11 | part 'src/draggable_manager.dart'; 12 | part 'src/dropzone.dart'; 13 | part 'src/dropzone_acceptor.dart'; 14 | -------------------------------------------------------------------------------- /lib/src/draggable.dart: -------------------------------------------------------------------------------- 1 | part of dnd; 2 | 3 | /// Info about the current drag operation. Must be a top-level variable so that 4 | /// all [Draggable]s know when a drag operation is already handled. 5 | /// Is also used by the [Dropzone] because we can't pass an [Element] through 6 | /// the [CustomEvent]s. 7 | _DragInfo? _currentDrag; 8 | 9 | /// The [Draggable] detects drag operations for touch and mouse interactions and 10 | /// optionally creates a drag avatar for visual feedback of the drag. Event 11 | /// streams are provided to track touch or mouse dragging: 12 | /// 13 | /// * [onDragStart] 14 | /// * [onDrag] 15 | /// * [onDragEnd] 16 | /// 17 | /// A [Draggable] can be created for one [Element] or an [ElementList]. 18 | class Draggable { 19 | /// Counter to generate a unique id for each instance. 20 | static int idCounter = 0; 21 | 22 | /// An auto-generated [id] used to identify this [Draggable]. 23 | final int id = idCounter++; 24 | 25 | // -------------- 26 | // Options 27 | // -------------- 28 | /// [avatarHandler] is a function to create a [DragAvatar] for this [Draggable]. 29 | /// See [Draggable] constructor. 30 | AvatarHandler? avatarHandler; 31 | 32 | /// When set to true, only horizontal dragging is tracked. This enables 33 | /// vertical touch dragging to be used for scrolling. 34 | bool horizontalOnly; 35 | 36 | /// When set to true, only vertical dragging is tracked. This enables 37 | /// horizontal touch dragging to be used for scrolling. 38 | bool verticalOnly; 39 | 40 | /// Restricts dragging from starting to the [handle]. 41 | /// See [Draggable] constructor. 42 | String? handle; 43 | 44 | /// Prevents dragging from starting on specified elements. 45 | /// See [Draggable] constructor. 46 | String cancel; 47 | 48 | /// CSS class set to the dragged element during a drag. 49 | /// See [Draggable] constructor. 50 | String draggingClass; 51 | 52 | /// CSS class set to the html body tag during a drag. 53 | /// See [Draggable] constructor. 54 | String draggingClassBody; 55 | 56 | /// The minimum distance in pixels that is needed for a drag to start. 57 | /// See [Draggable] constructor. 58 | int minDragStartDistance; 59 | 60 | /// The minimum distance for a drag to prevent click events. 61 | /// DEPRECATED: Please use [minDragStartDistance] instead. 62 | @deprecated 63 | int get clickSuppression => minDragStartDistance; 64 | 65 | /// DEPRECATED: Please use [minDragStartDistance] instead. 66 | @deprecated 67 | set clickSuppression(int distance) => minDragStartDistance = distance; 68 | 69 | // ------------------- 70 | // Events 71 | // ------------------- 72 | StreamController? _onDragStart; 73 | StreamController? _onDrag; 74 | StreamController? _onDragEnd; 75 | 76 | /// Fired when the user starts dragging. 77 | /// 78 | /// Note: The [onDragStart] is fired not on touchStart or mouseDown but as 79 | /// soon as there is a drag movement. When a drag is started an [onDrag] event 80 | /// will also be fired. 81 | Stream get onDragStart { 82 | if (_onDragStart == null) { 83 | _onDragStart = StreamController.broadcast( 84 | sync: true, onCancel: () => _onDragStart = null); 85 | } 86 | return _onDragStart!.stream; 87 | } 88 | 89 | /// Fired periodically throughout the drag operation. 90 | Stream get onDrag { 91 | if (_onDrag == null) { 92 | _onDrag = StreamController.broadcast( 93 | sync: true, onCancel: () => _onDrag = null); 94 | } 95 | return _onDrag!.stream; 96 | } 97 | 98 | /// Fired when the user ends the dragging. 99 | /// Is also fired when the user clicks the 'esc'-key or the window loses focus. 100 | Stream get onDragEnd { 101 | if (_onDragEnd == null) { 102 | _onDragEnd = StreamController.broadcast( 103 | sync: true, onCancel: () => _onDragEnd = null); 104 | } 105 | return _onDragEnd!.stream; 106 | } 107 | 108 | /// Abort the current drag 109 | void abort() { 110 | if (_currentDrag != null && _currentDrag!.draggableId == this.id) { 111 | _handleDragEnd(null, null, cancelled: true); 112 | } 113 | } 114 | 115 | // ------------------- 116 | // Private Properties 117 | // ------------------- 118 | /// The list of [Element]s on which a drag is detected. 119 | late List _elements; 120 | 121 | /// Managers for browser events. 122 | final List<_EventManager> _eventManagers = []; 123 | 124 | /// Creates a new [Draggable] for [elementOrElementList]. The 125 | /// [elementOrElementList] must be of type [Element], [ElementList], or 126 | /// [List]. 127 | /// 128 | /// 129 | /// ## Options 130 | /// 131 | /// The [avatarHandler] is responsible for creating, position, and 132 | /// removing a drag avatar. A drag avatar provides visual feedback during a 133 | /// drag operation. Here are possible options for the [avatarHandler] : 134 | /// 135 | /// * null (the default) - will not create a drag avatar 136 | /// * [AvatarHandler.original] - handler that uses the original 137 | /// draggable as avatar. See [OriginalAvatarHandler]. 138 | /// * [AvatarHandler.clone] - handler that uses a clone of the draggable 139 | /// element as avatar. See [CloneAvatarHandler]. 140 | /// * A custom [AvatarHandler] - you can provide your own implementation of 141 | /// [AvatarHandler]. 142 | /// 143 | /// If [horizontalOnly] is set to true, only horizontal dragging is tracked. 144 | /// This enables vertical touch dragging to be used for scrolling. 145 | /// 146 | /// If [verticalOnly] is set to true, only vertical dragging is tracked. 147 | /// This enables horizontal touch dragging to be used for scrolling. 148 | /// 149 | /// If a [handle] query String is specified, it restricts the dragging from 150 | /// starting unless it occurs on the specified element(s). Only elements that 151 | /// descend from [elementOrElementList] are permitted. 152 | /// 153 | /// If [cancel] query String is specified, drag starting is prevented on 154 | /// specified elements. 155 | /// 156 | /// The [draggingClass] is the css class set to the dragged element 157 | /// during a drag. If set to null, no such css class is added. 158 | /// 159 | /// The [draggingClassBody] is the css class set to the html body tag 160 | /// during a drag. If set to null, no such css class is added. 161 | /// 162 | /// The [minDragStartDistance] is the minimum distance in pixels that is needed 163 | /// for a drag to start. Default is `4`. This allows for clicks with tiny movement. 164 | /// 165 | Draggable(elementOrElementList, 166 | {this.avatarHandler, 167 | this.horizontalOnly = false, 168 | this.verticalOnly = false, 169 | this.handle, 170 | this.cancel = 'input, textarea, button, select, option', 171 | this.draggingClass = 'dnd-dragging', 172 | this.draggingClassBody = 'dnd-drag-occurring', 173 | this.minDragStartDistance = 4}) { 174 | // Wrap in a List if it is not a list but a single Element. 175 | _elements = elementOrElementList is List 176 | ? elementOrElementList 177 | : [elementOrElementList]; 178 | 179 | // Detect Pointer Event Support. 180 | JsObject jsWindow = JsObject.fromBrowserObject(window); 181 | 182 | if (jsWindow.hasProperty('PointerEvent')) { 183 | // Browser with support for Pointer Events. 184 | _eventManagers.add(_PointerManager(this)); 185 | } else { 186 | // We're on a browser with no support for Pointer Events. 187 | // Install touch and mouse listeners. 188 | if (TouchEvent.supported) { 189 | _eventManagers.add(_TouchManager(this)); 190 | } 191 | _eventManagers.add(_MouseManager(this)); 192 | } 193 | } 194 | 195 | /// Handles the drag start. The [moveEvent] might either be a 196 | /// [TouchEvent] or a [MouseEvent]. 197 | void _handleDragStart(Event moveEvent) { 198 | // Set the drag started flag. 199 | _currentDrag!.started = true; 200 | 201 | // Pass event to AvatarHandler. 202 | if (avatarHandler != null) { 203 | avatarHandler! 204 | ._handleDragStart(_currentDrag!.element, _currentDrag!.position); 205 | } 206 | 207 | // Fire the drag start event with start position. 208 | if (_onDragStart != null) { 209 | // The dragStart has the same for startPosition and current position. 210 | _onDragStart!.add(DraggableEvent._(moveEvent, _currentDrag!)); 211 | } 212 | 213 | // Add the css classes during the drag operation. 214 | _currentDrag!.element.classes.add(draggingClass); 215 | document.body!.classes.add(draggingClassBody); 216 | 217 | // Text selections should not be a problem, but it seems better usability 218 | // to remove text selection when dragging something. 219 | _clearTextSelections(); 220 | } 221 | 222 | /// Handles the drag. The [moveEvent] might either be a [TouchEvent] or a 223 | /// [MouseEvent]. 224 | /// 225 | /// The [target] is the actual target receiving the event. 226 | void _handleDrag(Event moveEvent, EventTarget? target) { 227 | // Pass event to AvatarHandler. 228 | if (avatarHandler != null) { 229 | avatarHandler! 230 | ._handleDrag(_currentDrag!.startPosition, _currentDrag!.position); 231 | } 232 | 233 | // Dispatch internal drag enter, over, or leave event. 234 | _DragEventDispatcher.dispatchEnterOverLeave(this, target); 235 | 236 | // Fire the drag event. 237 | if (_onDrag != null) { 238 | _onDrag!.add(DraggableEvent._(moveEvent, _currentDrag!)); 239 | } 240 | } 241 | 242 | /// Handles the drag end (mouseUp or touchEnd) event. The [event] might either 243 | /// be a [TouchEvent], a [MouseEvent], a [KeyboardEvent], or a [Event] (when 244 | /// focus is lost). 245 | /// 246 | /// The [target] is the actual target receiving the event. The [target] may 247 | /// be null when the event was [cancelled] (e.g. user clicked esc-key). 248 | /// 249 | /// Set [cancelled] to true to indicate that this drag ended through a 250 | /// cancel oparation like hitting the `esc` key. 251 | void _handleDragEnd(Event? event, EventTarget? target, {cancelled = false}) { 252 | // Only handle drag end if the user actually did drag and not just clicked. 253 | if (_currentDrag != null && _currentDrag!.started) { 254 | // Pass event to AvatarHandler. 255 | if (avatarHandler != null) { 256 | avatarHandler!._handleDragEnd( 257 | _currentDrag!.startPosition, _currentDrag!.position); 258 | } 259 | 260 | // Dispatch internal drop event if drag was not cancelled. 261 | if (!cancelled && target != null) { 262 | _DragEventDispatcher.dispatchDrop(this, target); 263 | } 264 | 265 | // Fire dragEnd event. 266 | if (_onDragEnd != null) { 267 | _onDragEnd! 268 | .add(DraggableEvent._(event, _currentDrag!, cancelled: cancelled)); 269 | } 270 | 271 | // Prevent TouchEvent from emulating a click after touchEnd event. 272 | if (event != null) { 273 | event.preventDefault(); 274 | } 275 | 276 | // Prevent MouseEvent from firing a click after mouseUp event. 277 | if (event is MouseEvent) { 278 | _suppressClickEvent(_currentDrag!.element); 279 | } 280 | 281 | // Remove the css classes. 282 | _currentDrag!.element.classes.remove(draggingClass); 283 | document.body!.classes.remove(draggingClassBody); 284 | } 285 | 286 | // Reset. 287 | _resetCurrentDrag(); 288 | } 289 | 290 | /// Makes sure that a potential click event is ignored. This is necessary for 291 | /// [MouseEvent]s. We have to wait for and cancel a potential click event 292 | /// happening after the mouseUp event. 293 | void _suppressClickEvent(Element element) { 294 | StreamSubscription clickPreventer = element.onClick.listen((event) { 295 | event.stopPropagation(); 296 | event.preventDefault(); 297 | }); 298 | 299 | // Wait until the end of event loop to see if a click event is fired. 300 | // Then cancel the listener. 301 | Future(() { 302 | clickPreventer.cancel(); 303 | }); 304 | } 305 | 306 | /// Resets the draggable elements to their initial state. 307 | /// 308 | /// All listeners are uninstalled. 309 | void destroy() { 310 | _resetCurrentDrag(); 311 | 312 | // Destroy all managers with their listeners. 313 | _eventManagers.forEach((m) => m.destroy()); 314 | _eventManagers.clear(); 315 | if (avatarHandler != null && avatarHandler!.avatar != null) { 316 | avatarHandler!.avatar!.remove(); 317 | } 318 | } 319 | 320 | /// Cancels drag subscriptions and resets to initial state. 321 | void _resetCurrentDrag() { 322 | // Reset all managers. 323 | _eventManagers.forEach((m) => m.reset()); 324 | 325 | // Reset dispatcher to fire a last internal dragLeave event. 326 | _DragEventDispatcher.reset(); 327 | 328 | // Reset the current drag. 329 | _currentDrag = null; 330 | } 331 | 332 | /// Removes all text selections from the HTML document, including selections 333 | /// in active textarea or active input element. 334 | void _clearTextSelections() { 335 | // Remove selection. 336 | window.getSelection()?.removeAllRanges(); 337 | 338 | // Try to remove selection from textarea or input. 339 | try { 340 | var activeElement = document.activeElement; 341 | if (activeElement is TextAreaElement) { 342 | activeElement.setSelectionRange(0, 0); 343 | } else if (activeElement is InputElement) { 344 | activeElement.setSelectionRange(0, 0); 345 | } 346 | } catch (_) { 347 | // Might throw an error if the element does not support setSelectionRange. 348 | // This is the case for InputElement with type 'file'. 349 | } 350 | } 351 | } 352 | 353 | /// Event used when a drag is detected. 354 | class DraggableEvent { 355 | /// The [Element] that is beeing dragged. 356 | final Element draggableElement; 357 | 358 | /// The [AvatarHandler] or null if there is none. 359 | final AvatarHandler? avatarHandler; 360 | 361 | /// The original event which is either ... 362 | /// * a [MouseEvent], 363 | /// * a [TouchEvent], 364 | /// * a [KeyboardEvent] when the user clicks the esc-key, 365 | /// * a normal [Event] when the window loses focus (blur event), or 366 | /// * null if the drag was programmatically aborted. 367 | final Event? originalEvent; 368 | 369 | /// Position where the drag started, relative to the whole document (page 370 | /// position). 371 | final Point startPosition; 372 | 373 | /// The current mouse/touch position, relative to the whole document (page 374 | /// position). 375 | final Point position; 376 | 377 | /// Indicates if this [DraggableEvent] was [cancelled]. This is currently 378 | /// only used for [onDragEnd] events to indicate a drag end through a 379 | /// cancelling oparation like `esc` key or windows loosing focus. 380 | final bool cancelled; 381 | 382 | /// Private constructor for [DraggableEvent]. 383 | DraggableEvent._(this.originalEvent, _DragInfo dragInfo, 384 | {this.cancelled = false}) 385 | : draggableElement = dragInfo.element, 386 | avatarHandler = dragInfo.avatarHandler, 387 | startPosition = dragInfo.startPosition, 388 | position = dragInfo.position; 389 | } 390 | 391 | /// Information about the current drag operation. 392 | class _DragInfo { 393 | /// The id of the currently dragged [Draggable]. 394 | final int draggableId; 395 | 396 | /// The dragged element. 397 | final Element element; 398 | 399 | /// Position where the drag started. 400 | final Point startPosition; 401 | 402 | /// The [AvatarHandler] or null if there is none. 403 | final AvatarHandler? avatarHandler; 404 | 405 | /// The current position of the mouse or touch. This position is constrained 406 | /// by the horizontal/vertical axis. 407 | late Point _position; 408 | 409 | /// Flag indicating if the drag started. 410 | bool started = false; 411 | 412 | final bool horizontalOnly; 413 | final bool verticalOnly; 414 | 415 | _DragInfo(this.draggableId, this.element, this.startPosition, 416 | {this.avatarHandler, 417 | this.horizontalOnly = false, 418 | this.verticalOnly = false}) { 419 | // Initially set current position to startPosition. 420 | _position = startPosition; 421 | } 422 | 423 | /// The current position, constrained by the horizontal/vertical axis 424 | /// depending on [horizontalOnly] and [verticalOnly]. 425 | Point get position => _position; 426 | 427 | /// Sets the current position. 428 | set position(Point pos) => _position = _constrainAxis(pos); 429 | 430 | /// Constrains the axis if [horizontalOnly] or [verticalOnly] is true. 431 | Point _constrainAxis(Point pos) { 432 | // Set y-value to startPosition if horizontalOnly. 433 | if (horizontalOnly) { 434 | return Point(pos.x, startPosition.y); 435 | } 436 | 437 | // Set x-value to startPosition if verticalOnly. 438 | if (verticalOnly) { 439 | return Point(startPosition.x, pos.y); 440 | } 441 | 442 | // Axis is not constrained. 443 | return pos; 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /lib/src/draggable_avatar.dart: -------------------------------------------------------------------------------- 1 | part of dnd; 2 | 3 | /// The [AvatarHandler] is responsible for creating, position, and removing 4 | /// a drag avatar. A drag avatar provides visual feedback during the drag 5 | /// operation. 6 | abstract class AvatarHandler { 7 | /// Returns the [avatar] element during a drag operation. 8 | /// 9 | /// If there is no drag operation going on, [avatar] will be null. 10 | Element? avatar; 11 | 12 | /// The cached top margin of [avatar]. 13 | num? _marginTop; 14 | 15 | /// Returns the (cached) top margin of [avatar]. 16 | num? get marginTop { 17 | if (_marginTop == null) { 18 | cacheMargins(); 19 | } 20 | return _marginTop; 21 | } 22 | 23 | /// The cached left margin of [avatar]. 24 | num? _marginLeft; 25 | 26 | /// Returns the (cached) left margin of [avatar]. 27 | num? get marginLeft { 28 | if (_marginLeft == null) { 29 | cacheMargins(); 30 | } 31 | return _marginLeft; 32 | } 33 | 34 | /// Saved pointer events style before the drag operation. 35 | String? _pointerEventsBeforeDrag; 36 | 37 | /// Default constructor. 38 | AvatarHandler(); 39 | 40 | /// Creates an [AvatarHelper] that uses the draggable element itself as 41 | /// drag avatar. 42 | /// 43 | /// See [OriginalAvatarHandler]. 44 | factory AvatarHandler.original() { 45 | return OriginalAvatarHandler(); 46 | } 47 | 48 | /// Creates an [AvatarHelper] that creates a clone of the draggable element 49 | /// as drag avatar. The avatar is removed at the end of the drag operation. 50 | /// 51 | /// See [CloneAvatarHandler]. 52 | factory AvatarHandler.clone() { 53 | return CloneAvatarHandler(); 54 | } 55 | 56 | /// Handles the drag start. 57 | void _handleDragStart(Element draggable, Point startPosition) { 58 | dragStart(draggable, startPosition); 59 | 60 | // Sets the pointer-events CSS property of avatar to 'none' which enables 61 | // mouse and touch events to go trough to the element under the avatar. 62 | if (avatar != null) { 63 | _pointerEventsBeforeDrag = avatar!.style.pointerEvents; 64 | avatar!.style.pointerEvents = 'none'; 65 | } 66 | } 67 | 68 | /// Handles the drag. 69 | void _handleDrag(Point startPosition, Point position) { 70 | drag(startPosition, position); 71 | } 72 | 73 | /// Handles the drag end. 74 | void _handleDragEnd(Point startPosition, Point position) { 75 | dragEnd(startPosition, position); 76 | 77 | // Reset the pointer-events CSS property to its original value. 78 | if (avatar != null) { 79 | avatar!.style.pointerEvents = _pointerEventsBeforeDrag ?? ''; 80 | } 81 | _pointerEventsBeforeDrag = null; 82 | 83 | // Reset avatar. 84 | avatar = null; 85 | 86 | // Reset margins (causes them to be recalculated in next drag operation). 87 | _marginTop = null; 88 | _marginLeft = null; 89 | } 90 | 91 | /// Called when the drag operation starts. 92 | /// 93 | /// This method must set the [avatar] variable and must attache it to the DOM. 94 | /// 95 | /// The provided [draggable] is used to know where in the DOM the drag avatar 96 | /// can be inserted. 97 | /// 98 | /// The [startPosition] is the position where the drag started, relative to the 99 | /// whole document (page coordinates). 100 | void dragStart(Element draggable, Point startPosition); 101 | 102 | /// Moves the drag avatar to the new [position]. 103 | /// 104 | /// The [startPosition] is the position where the drag started, [position] is the 105 | /// current position. Both are relative to the whole document (page coordinates). 106 | void drag(Point startPosition, Point position); 107 | 108 | /// Called when the drag operation ends. 109 | /// 110 | /// The [avatar] must be removed from the DOM in this method if it is not 111 | /// needed any more. 112 | /// 113 | /// The [startPosition] is the position where the drag started, [position] is the 114 | /// current position. Both are relative to the whole document (page coordinates). 115 | void dragEnd(Point startPosition, Point position); 116 | 117 | /// Sets the CSS transform translate of [avatar]. Uses requestAnimationFrame 118 | /// to speed up animation. 119 | void setTranslate(Point position) { 120 | // Use request animation frame to update the transform translate. 121 | AnimationHelper.requestUpdate(() { 122 | // Unsing `translate3d` to activate GPU hardware-acceleration (a bit of a hack). 123 | if (avatar != null) { 124 | avatar!.style.transform = 125 | 'translate3d(${position.x}px, ${position.y}px, 0)'; 126 | } 127 | }); 128 | } 129 | 130 | /// Removes the CSS transform of [avatar]. Also stops the requested animation 131 | /// from [setTranslate]. 132 | void removeTranslate() { 133 | AnimationHelper.stop(); 134 | if (avatar != null) { 135 | avatar!.style.transform = ''; 136 | } 137 | } 138 | 139 | /// Sets the CSS left/top values of [avatar]. Takes care of any left/top 140 | /// margins the [avatar] might have to correctly position the element. 141 | /// 142 | /// Note: The [avatar] must already be in the DOM for the margins to be 143 | /// calculated correctly. 144 | void setLeftTop(Point position) { 145 | if (avatar != null) { 146 | avatar!.style.left = '${position.x - (marginLeft ?? 0)}px'; 147 | avatar!.style.top = '${position.y - (marginTop ?? 0)}px'; 148 | } 149 | } 150 | 151 | /// Caches the [marginLeft] and [marginTop] of [avatar]. 152 | /// 153 | /// Call this method again if those margins somehow changed during a drag 154 | /// operation. 155 | void cacheMargins() { 156 | // Calculate margins. 157 | if (avatar != null) { 158 | var computedStyles = avatar!.getComputedStyle(); 159 | _marginLeft = 160 | num.tryParse(computedStyles.marginLeft.replaceFirst('px', '')) ?? 0; 161 | _marginTop = 162 | num.tryParse(computedStyles.marginTop.replaceFirst('px', '')) ?? 0; 163 | } 164 | } 165 | } 166 | 167 | /// The [OriginalAvatarHandler] uses the draggable element itself as drag 168 | /// avatar. It uses absolute positioning of the avatar. 169 | class OriginalAvatarHandler extends AvatarHandler { 170 | Point? _draggableStartOffset; 171 | 172 | @override 173 | void dragStart(Element draggable, Point startPosition) { 174 | // Use the draggable itself as avatar. 175 | avatar = draggable; 176 | 177 | // Ensure avatar has an absolute position. 178 | if (avatar != null) { 179 | avatar!.style.position = 'absolute'; 180 | } 181 | 182 | // Get the start offset of the draggable (relative to the closest positioned 183 | // ancestor). 184 | _draggableStartOffset = draggable.offset.topLeft; 185 | 186 | // Set the initial position of the original. 187 | setLeftTop(_draggableStartOffset!); 188 | } 189 | 190 | @override 191 | void drag(Point startPosition, Point position) { 192 | setTranslate(position - startPosition); 193 | } 194 | 195 | @override 196 | void dragEnd(Point startPosition, Point position) { 197 | // Remove the translate and set the new position as left/top. 198 | removeTranslate(); 199 | 200 | // Set the new position as left/top. Prevent from moving past the top and 201 | // left borders as the user might not be able to grab the element any more. 202 | Point constrainedPosition = 203 | Point(math.max(1, position.x), math.max(1, position.y)); 204 | 205 | setLeftTop(constrainedPosition - startPosition + _draggableStartOffset!); 206 | } 207 | } 208 | 209 | /// [CloneAvatarHandler] creates a clone of the draggable element as drag avatar. 210 | /// The avatar is removed at the end of the drag operation. 211 | class CloneAvatarHandler extends AvatarHandler { 212 | @override 213 | void dragStart(Element draggable, Point startPosition) { 214 | // Clone the draggable to create the avatar. 215 | avatar = (draggable.clone(true) as Element) 216 | ..attributes.remove('id') 217 | ..style.cursor = 'inherit'; 218 | 219 | // Ensure avatar has an absolute position. 220 | avatar!.style.position = 'absolute'; 221 | avatar!.style.zIndex = '100'; 222 | 223 | // Add the drag avatar to the parent element. 224 | draggable.parentNode!.append(avatar!); 225 | 226 | // Set the initial position of avatar (relative to the closest positioned 227 | // ancestor). 228 | setLeftTop(draggable.offset.topLeft); 229 | } 230 | 231 | @override 232 | void drag(Point startPosition, Point position) { 233 | setTranslate(position - startPosition); 234 | } 235 | 236 | @override 237 | void dragEnd(Point startPosition, Point position) { 238 | if (avatar != null) { 239 | avatar!.remove(); 240 | } 241 | } 242 | } 243 | 244 | /// Simple helper class to speed up animation with requestAnimationFrame. 245 | class AnimationHelper { 246 | static Function? _lastUpdateFunction; 247 | static bool _updating = false; 248 | 249 | /// Requests that the [updateFunction] be called. When the animation frame is 250 | /// ready, the [updateFunction] is executed. Note that any subsequent calls 251 | /// in the same frame will overwrite the [updateFunction]. 252 | static void requestUpdate(void updateFunction()) { 253 | _lastUpdateFunction = updateFunction; 254 | 255 | if (!_updating) { 256 | window.animationFrame.then((_) => _update()); 257 | _updating = true; 258 | } 259 | } 260 | 261 | /// Stops the updating. 262 | static void stop() { 263 | _updating = false; 264 | } 265 | 266 | static void _update() { 267 | // Test if it wasn't stopped. 268 | if (_updating) { 269 | if (_lastUpdateFunction != null) { 270 | _lastUpdateFunction!(); 271 | } 272 | _updating = false; 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /lib/src/draggable_dispatch.dart: -------------------------------------------------------------------------------- 1 | part of dnd; 2 | 3 | /// Dispatches [MouseEvent]s for dragEnter, dragOver, and dragLeave. 4 | /// 5 | /// Those events are only meant for communication between [Draggable]s and 6 | /// [Dropzone]s and not to be consumed by users of the library. 7 | class _DragEventDispatcher { 8 | /// Custom drag enter event that is fired on the element that is entered. 9 | static const String CUSTOM_DRAG_ENTER = '_customDragEnter'; 10 | 11 | /// Custom drag over event that is fired on the element that is dragged over. 12 | static const String CUSTOM_DRAG_OVER = '_customDragOver'; 13 | 14 | /// Custom drag leave event that is fired on the element that is left. 15 | static const String CUSTOM_DRAG_LEAVE = '_customDragLeave'; 16 | 17 | /// Custom drop event that is fired on the element that the draggable was dropped. 18 | static const String CUSTOM_DROP = '_customDrop'; 19 | 20 | /// Stream provider for [CUSTOM_DRAG_ENTER] events. The relatedTarget contains 21 | /// the [Element] the user entered from (may be null). 22 | static EventStreamProvider enterEvent = 23 | EventStreamProvider(CUSTOM_DRAG_ENTER); 24 | 25 | /// Stream provider for [CUSTOM_DRAG_OVER] events. The relatedTarget is empty. 26 | static EventStreamProvider overEvent = 27 | EventStreamProvider(CUSTOM_DRAG_OVER); 28 | 29 | /// Stream provider for [CUSTOM_DRAG_LEAVE] events. The relatedTarget contains 30 | /// the [Element] the user is leaving to (may be null). 31 | static EventStreamProvider leaveEvent = 32 | EventStreamProvider(CUSTOM_DRAG_LEAVE); 33 | 34 | /// Stream provider for [CUSTOM_DROP] events. The relatedTarget is empty. 35 | static EventStreamProvider dropEvent = 36 | EventStreamProvider(CUSTOM_DROP); 37 | 38 | /// Keeps track of the previous target to be able to fire dragLeave events on it. 39 | static EventTarget? previousTarget; 40 | 41 | /// Dispatches dragEnter, dragOver, and dragLeave events. 42 | /// 43 | /// The [draggable] is the [Draggable] that is dispatching the event. 44 | /// The [target] is the element that the event will be dispatched on. 45 | static void dispatchEnterOverLeave(Draggable draggable, EventTarget? target) { 46 | // Sometimes the target is null (e.g. when user drags over buttons on 47 | // android). Ignore it. 48 | if (target == null) { 49 | return; 50 | } 51 | 52 | if (previousTarget == target) { 53 | // Moved on the same element --> dispatch dragOver. 54 | MouseEvent dragOverEvent = MouseEvent(CUSTOM_DRAG_OVER); 55 | target.dispatchEvent(dragOverEvent); 56 | } else { 57 | // Entered a new element --> fire dragEnter of new element. 58 | MouseEvent dragEnterEvent = 59 | MouseEvent(CUSTOM_DRAG_ENTER, relatedTarget: previousTarget); 60 | target.dispatchEvent(dragEnterEvent); 61 | 62 | // Fire dragLeave of old element (if there is one). 63 | if (previousTarget != null) { 64 | MouseEvent dragLeaveEvent = 65 | MouseEvent(CUSTOM_DRAG_LEAVE, relatedTarget: target); 66 | previousTarget!.dispatchEvent(dragLeaveEvent); 67 | } 68 | 69 | // Also fire the first dragOver event for the new element. 70 | MouseEvent dragOverEvent = MouseEvent(CUSTOM_DRAG_OVER); 71 | target.dispatchEvent(dragOverEvent); 72 | 73 | previousTarget = target; 74 | } 75 | } 76 | 77 | /// Dispatches drop event. 78 | /// 79 | /// The [draggable] is the [Draggable] that is dispatching the event. 80 | /// The [target] is the element that the event will be dispatched on. 81 | static void dispatchDrop(Draggable draggable, EventTarget? target) { 82 | // Sometimes the target is null (e.g. when user drags over buttons on 83 | // android). Ignore it. 84 | if (target == null) { 85 | return; 86 | } 87 | 88 | MouseEvent dropEvent = MouseEvent(CUSTOM_DROP); 89 | target.dispatchEvent(dropEvent); 90 | 91 | reset(); 92 | } 93 | 94 | /// Must be called when drag ended to fire a last dragLeave event. 95 | static void reset() { 96 | // Fire a last dragLeave. 97 | if (previousTarget != null) { 98 | MouseEvent dragLeaveEvent = MouseEvent(CUSTOM_DRAG_LEAVE); 99 | previousTarget!.dispatchEvent(dragLeaveEvent); 100 | previousTarget = null; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/draggable_manager.dart: -------------------------------------------------------------------------------- 1 | part of dnd; 2 | 3 | /// Class responsible for managing browser events. 4 | /// 5 | /// This class is an abstraction for the specific managers 6 | /// [_TouchManager], [_MouseManager], and [_PointerManager]. 7 | abstract class _EventManager { 8 | /// Attribute to mark custom elements where events should be retargeted 9 | /// to their Shadow DOM children. 10 | static const String SHADOW_DOM_RETARGET_ATTRIBUTE = 'dnd-retarget'; 11 | 12 | /// Tracks subscriptions for start events (mouseDown, touchStart). 13 | List startSubs = []; 14 | 15 | /// Tracks subscriptions for all other events (mouseMove, touchMove, mouseUp, 16 | /// touchEnd, and more). 17 | List dragSubs = []; 18 | 19 | /// A reference back to the [Draggable]. 20 | final Draggable drg; 21 | 22 | _EventManager(this.drg) { 23 | // Install the start listeners when constructed. 24 | installStart(); 25 | 26 | // Disable touch actions (scrolling, panning, zooming) depending on 27 | // horizontalOnly / verticalOnly options. 28 | if (drg.horizontalOnly) { 29 | // Only allow vertical scrolling, panning. 30 | drg._elements.forEach((el) => el.style.touchAction = 'pan-y'); 31 | } else if (drg.verticalOnly) { 32 | // Only allow horizontal scrolling, panning. 33 | drg._elements.forEach((el) => el.style.touchAction = 'pan-x'); 34 | } else { 35 | // No scrolling, panning. 36 | drg._elements.forEach((el) => el.style.touchAction = 'none'); 37 | } 38 | } 39 | 40 | /// Installs the start listeners (e.g. mouseDown, touchStart, etc.). 41 | void installStart(); 42 | 43 | /// Installs the move listeners (e.g. mouseMove, touchMove, etc.). 44 | void installMove(); 45 | 46 | /// Installs the end listeners (e.g. mouseUp, touchEnd, etc.). 47 | void installEnd(); 48 | 49 | /// Installs the cancel listeners (e.g. touchCancel, pointerCancel, etc.). 50 | void installCancel(); 51 | 52 | /// Installs listener for esc-key and blur (window loses focus). Those 53 | /// events will cancel the drag operation. 54 | void installEscAndBlur() { 55 | // Drag ends when escape key is hit. 56 | dragSubs.add(window.onKeyDown.listen((keyboardEvent) { 57 | if (keyboardEvent.keyCode == KeyCode.ESC) { 58 | handleCancel(keyboardEvent); 59 | } 60 | })); 61 | 62 | // Drag ends when focus is lost. 63 | dragSubs.add(window.onBlur.listen((event) { 64 | handleCancel(event); 65 | })); 66 | } 67 | 68 | /// Handles a start event (touchStart, mouseUp, etc.). 69 | void handleStart(Event event, Point position) { 70 | // Initialize the drag info. 71 | // Note: the drag is not started on touchStart but after a first valid move. 72 | _currentDrag = _DragInfo(drg.id, event.currentTarget as Element, position, 73 | avatarHandler: drg.avatarHandler, 74 | horizontalOnly: drg.horizontalOnly, 75 | verticalOnly: drg.verticalOnly); 76 | 77 | // Install listeners to detect a drag move, end, or cancel. 78 | installMove(); 79 | installEnd(); 80 | installCancel(); 81 | installEscAndBlur(); 82 | } 83 | 84 | /// Handles a move event (touchMove, mouseMove, etc.). 85 | void handleMove(Event event, Point position, Point clientPosition) { 86 | // Set the current position. 87 | _currentDrag!.position = position; 88 | 89 | if (!_currentDrag!.started) { 90 | // Test if drag has moved far enough to start drag. 91 | if (_currentDrag!.startPosition.distanceTo(_currentDrag!.position) >= 92 | drg.minDragStartDistance) { 93 | // Drag starts now. 94 | drg._handleDragStart(event); 95 | } 96 | } else { 97 | // Drag already started. 98 | Element realTarget = _getRealTarget(clientPosition); 99 | drg._handleDrag(event, realTarget); 100 | } 101 | } 102 | 103 | /// Handles all end events (touchEnd, mouseUp, and pointerUp). 104 | void handleEnd(Event event, EventTarget? target, Point? position, 105 | Point? clientPosition) { 106 | // Set the current position. 107 | if (position != null && _currentDrag != null) { 108 | _currentDrag!.position = position; 109 | } 110 | 111 | if (clientPosition != null) { 112 | EventTarget realTarget = _getRealTarget(clientPosition, target: target); 113 | drg._handleDragEnd(event, realTarget); 114 | } 115 | } 116 | 117 | /// Handles all cancel events (touchCancel and pointerCancel). 118 | void handleCancel(Event event) { 119 | // Drag end with the cancelled flag. 120 | drg._handleDragEnd(event, null, cancelled: true); 121 | } 122 | 123 | /// Resets this [_EventManager] to its initial state. This means that all 124 | /// listeners are canceled except the listeners set up during [installStart]. 125 | void reset() { 126 | // Cancel drag subscriptions. 127 | dragSubs.forEach((sub) => sub.cancel()); 128 | dragSubs.clear(); 129 | } 130 | 131 | /// Cancels all listeners, including the listeners set up during [installStart]. 132 | void destroy() { 133 | reset(); 134 | 135 | // Cancel start subscriptions. 136 | startSubs.forEach((sub) => sub.cancel()); 137 | startSubs.clear(); 138 | 139 | // Reset the touch action property. 140 | drg._elements.forEach((el) => el.style.touchAction = ''); 141 | } 142 | 143 | /// Determine a target using `document.elementFromPoint` via the provided [clientPosition]. 144 | /// 145 | /// Falls back to `document.body` if no element is found at the provided [clientPosition]. 146 | Element _getRealTargetFromPoint(Point clientPosition) { 147 | return document.elementFromPoint( 148 | clientPosition.x.round(), clientPosition.y.round()) ?? 149 | document.body!; 150 | } 151 | 152 | /// Determine the actual target that should receive the event because 153 | /// mouse or touch event might have occurred on a drag avatar. 154 | /// 155 | /// If a [target] is provided it is tested to see if is already the correct 156 | /// target or if it is the drag avatar and thus must be replaced by the 157 | /// element underneath. 158 | Element _getRealTarget(Point clientPosition, {EventTarget? target}) { 159 | // If no target was provided get it. 160 | if (target == null || target is! Element) { 161 | target = _getRealTargetFromPoint(clientPosition); 162 | } 163 | 164 | // Test if target is the drag avatar. 165 | if (drg.avatarHandler != null && 166 | drg.avatarHandler!.avatar != null && 167 | drg.avatarHandler!.avatar!.contains(target)) { 168 | // Target is the drag avatar, get element underneath. 169 | drg.avatarHandler!.avatar!.style.visibility = 'hidden'; 170 | target = _getRealTargetFromPoint(clientPosition); 171 | drg.avatarHandler!.avatar!.style.visibility = 'visible'; 172 | } 173 | 174 | target = _recursiveShadowDomTarget(clientPosition, target); 175 | 176 | return target; 177 | } 178 | 179 | /// Recursively searches for the real target inside the Shadow DOM for all 180 | /// Shadow hosts with the attribute [SHADOW_DOM_RETARGET_ATTRIBUTE]. 181 | Element _recursiveShadowDomTarget(Point clientPosition, Element target) { 182 | // Retarget if target is a shadow host and has the specific attribute. 183 | if (target is Element && 184 | target.shadowRoot != null && 185 | target.attributes.containsKey(SHADOW_DOM_RETARGET_ATTRIBUTE)) { 186 | Element? newTarget = target.shadowRoot! 187 | .elementFromPoint(clientPosition.x.round(), clientPosition.y.round()); 188 | 189 | // Recursive call for nested shadow DOM trees. 190 | if (newTarget != null) { 191 | target = _recursiveShadowDomTarget(clientPosition, newTarget); 192 | } 193 | } 194 | 195 | return target; 196 | } 197 | 198 | /// Tests if [target] is a valid place to start a drag. If [handle] is 199 | /// provided, drag can only start on the [handle]s. If [cancel] is 200 | /// provided, drag cannot be started on those elements. 201 | bool _isValidDragStartTarget(EventTarget target) { 202 | // Test if a drag was started on a cancel element. 203 | if (target is Element && target.matchesWithAncestors(drg.cancel)) { 204 | return false; 205 | } 206 | 207 | // If handle is specified, drag must start on handle or one of its children. 208 | if (drg.handle != null) { 209 | if (target is Element) { 210 | // 1. The target must match the handle query String. 211 | if (!target.matchesWithAncestors(drg.handle!)) { 212 | return false; 213 | } 214 | 215 | // 2. The target must be a child of the drag element(s). 216 | if (drg._elements.any((el) => el.contains(target))) { 217 | return true; 218 | } 219 | } 220 | 221 | // Has a handle specified but we did not find a match. 222 | return false; 223 | } 224 | 225 | return true; 226 | } 227 | } 228 | 229 | /// Manages the browser's touch events. 230 | class _TouchManager extends _EventManager { 231 | _TouchManager(Draggable draggable) : super(draggable); 232 | 233 | @override 234 | void installStart() { 235 | drg._elements.forEach((el) { 236 | startSubs.add(el.onTouchStart.listen((TouchEvent event) { 237 | // Ignore if drag is already beeing handled. 238 | if (_currentDrag != null) { 239 | return; 240 | } 241 | 242 | // Ignore multi-touch events. 243 | if (event.touches != null && event.touches!.length > 1) { 244 | return; 245 | } 246 | 247 | // Ensure the drag started on a valid target. 248 | if (event.touches != null && 249 | !_isValidDragStartTarget(event.touches![0].target!)) { 250 | return; 251 | } 252 | 253 | if (event.touches != null) { 254 | handleStart(event, event.touches![0].page); 255 | } 256 | })); 257 | }); 258 | } 259 | 260 | @override 261 | void installMove() { 262 | dragSubs.add(document.onTouchMove.listen((TouchEvent event) { 263 | // Stop and cancel subscriptions on multi-touch. 264 | if (event.touches != null && event.touches!.length > 1) { 265 | handleCancel(event); 266 | return; 267 | } 268 | 269 | // Do a scrolling test if this is the first drag. 270 | if (event.changedTouches != null) { 271 | if (!_currentDrag!.started && 272 | isScrolling(event.changedTouches![0].page)) { 273 | // The user is scrolling --> Stop tracking current drag. 274 | handleCancel(event); 275 | return; 276 | } 277 | 278 | handleMove(event, event.changedTouches![0].page, 279 | event.changedTouches![0].client); 280 | } 281 | 282 | // Prevent touch scrolling. 283 | event.preventDefault(); 284 | })); 285 | } 286 | 287 | @override 288 | void installEnd() { 289 | dragSubs.add(document.onTouchEnd.listen((TouchEvent event) { 290 | handleEnd(event, null, event.changedTouches?[0].page, 291 | event.changedTouches?[0].client); 292 | })); 293 | } 294 | 295 | @override 296 | void installCancel() { 297 | dragSubs.add(document.onTouchCancel.listen((TouchEvent event) { 298 | handleCancel(event); 299 | })); 300 | } 301 | 302 | /// Returns true if there was scrolling activity instead of dragging. 303 | bool isScrolling(Point currentPosition) { 304 | Point delta = currentPosition - _currentDrag!.startPosition; 305 | 306 | // If horizontalOnly test for vertical movement. 307 | if (drg.horizontalOnly && delta.y.abs() > delta.x.abs()) { 308 | // Vertical scrolling. 309 | return true; 310 | } 311 | 312 | // If verticalOnly test for horizontal movement. 313 | if (drg.verticalOnly && delta.x.abs() > delta.y.abs()) { 314 | // Horizontal scrolling. 315 | return true; 316 | } 317 | 318 | // No scrolling. 319 | return false; 320 | } 321 | } 322 | 323 | /// Manages the browser's mouse events. 324 | class _MouseManager extends _EventManager { 325 | _MouseManager(Draggable draggable) : super(draggable); 326 | 327 | @override 328 | void installStart() { 329 | drg._elements.forEach((el) { 330 | startSubs.add(el.onMouseDown.listen((MouseEvent event) { 331 | // Ignore if drag is already beeing handled. 332 | if (_currentDrag != null) { 333 | return; 334 | } 335 | 336 | // Only handle left clicks, ignore clicks from right or middle buttons. 337 | if (event.button != 0) { 338 | return; 339 | } 340 | 341 | // Ensure the drag started on a valid target. 342 | if (event.target != null && !_isValidDragStartTarget(event.target!)) { 343 | return; 344 | } 345 | 346 | // Prevent default on mouseDown. Reasons: 347 | // * Disables image dragging handled by the browser. 348 | // * Disables text selection. 349 | // 350 | // Note: We must NOT prevent default on form elements. Reasons: 351 | // * SelectElement would not show a dropdown. 352 | // * InputElement and TextAreaElement would not get focus. 353 | // * ButtonElement and OptionElement - don't know if this is needed?? 354 | EventTarget? target = event.target; 355 | if (!(target is SelectElement || 356 | target is InputElement || 357 | target is TextAreaElement || 358 | target is ButtonElement || 359 | target is OptionElement)) { 360 | event.preventDefault(); 361 | } 362 | 363 | handleStart(event, event.page); 364 | })); 365 | }); 366 | } 367 | 368 | @override 369 | void installMove() { 370 | dragSubs.add(document.onMouseMove.listen((MouseEvent event) { 371 | handleMove(event, event.page, event.client); 372 | })); 373 | } 374 | 375 | @override 376 | void installEnd() { 377 | dragSubs.add(document.onMouseUp.listen((MouseEvent event) { 378 | handleEnd(event, event.target, event.page, event.client); 379 | })); 380 | } 381 | 382 | @override 383 | void installCancel() { 384 | // No mouse cancel event. 385 | } 386 | } 387 | 388 | /// Manages the browser's pointer events (used for Internet Explorer). 389 | class _PointerManager extends _EventManager { 390 | _PointerManager(Draggable draggable) : super(draggable); 391 | 392 | @override 393 | void installStart() { 394 | drg._elements.forEach((el) { 395 | startSubs.add(el.on['pointerdown'].listen((e) { 396 | var event = e as PointerEvent; 397 | 398 | // Ignore if drag is already beeing handled. 399 | if (_currentDrag != null) { 400 | return; 401 | } 402 | 403 | // Only handle left clicks, ignore clicks from right or middle buttons. 404 | if (event.button != 0) { 405 | return; 406 | } 407 | 408 | // Ensure the drag started on a valid target. 409 | if (event.target != null && !_isValidDragStartTarget(event.target!)) { 410 | return; 411 | } 412 | 413 | // Prevent default on mouseDown. Reasons: 414 | // * Disables image dragging handled by the browser. 415 | // * Disables text selection. 416 | // 417 | // Note: We must NOT prevent default on form elements. Reasons: 418 | // * SelectElement would not show a dropdown. 419 | // * InputElement and TextAreaElement would not get focus. 420 | // * ButtonElement and OptionElement - don't know if this is needed?? 421 | EventTarget? target = event.target; 422 | if (!(target is SelectElement || 423 | target is InputElement || 424 | target is TextAreaElement || 425 | target is ButtonElement || 426 | target is OptionElement)) { 427 | event.preventDefault(); 428 | } 429 | 430 | handleStart(event, event.page); 431 | })); 432 | }); 433 | } 434 | 435 | @override 436 | void installMove() { 437 | dragSubs.add(document.on['pointermove'].listen((e) { 438 | var event = e as PointerEvent; 439 | handleMove(event, event.page, event.client); 440 | })); 441 | } 442 | 443 | @override 444 | void installEnd() { 445 | dragSubs.add(document.on['pointerup'].listen((e) { 446 | var event = e as PointerEvent; 447 | // handleEnd(event, event.target, event.page, event.client); 448 | handleEnd(event, null, event.page, event.client); 449 | })); 450 | } 451 | 452 | @override 453 | void installCancel() { 454 | dragSubs.add(document.on['pointercancel'].listen((event) { 455 | handleCancel(event); 456 | })); 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /lib/src/dropzone.dart: -------------------------------------------------------------------------------- 1 | part of dnd; 2 | 3 | /// The [Dropzone] detects when a [Draggable] is dragged over it or dropped on 4 | /// it. An [acceptor] can be provided to specify which [Draggable]s will be 5 | /// accepted. 6 | /// 7 | /// The following event streams are provided: 8 | /// 9 | /// * [onDragEnter] 10 | /// * [onDragOver] 11 | /// * [onDragLeave] 12 | /// * [onDrop] 13 | /// 14 | /// A [Dropzone] can be created for one [Element] or an [ElementList]. 15 | class Dropzone { 16 | // -------------- 17 | // Options 18 | // -------------- 19 | /// The [Acceptor] used to determine which [Draggable]s will be accepted by 20 | /// this [Dropzone]. If none is specified, all [Draggable]s will be accepted. 21 | Acceptor? acceptor; 22 | 23 | /// CSS class set to the [Dropzone] element when an accepted [Draggable] is 24 | /// dragged over it. See [Dropzone] constructor. 25 | String overClass; 26 | 27 | /// CSS class set to the [Dropzone] element when a not-accepted [Draggable] is 28 | /// dragged over it. See [Dropzone] constructor. 29 | String invalidClass; 30 | 31 | // ------------------- 32 | // Events 33 | // ------------------- 34 | StreamController? _onDragEnter; 35 | StreamController? _onDragOver; 36 | StreamController? _onDragLeave; 37 | StreamController? _onDrop; 38 | 39 | /// Fired when a [Draggable] enters this [Dropzone]. 40 | Stream get onDragEnter { 41 | if (_onDragEnter == null) { 42 | _onDragEnter = StreamController.broadcast( 43 | sync: true, onCancel: () => _onDragEnter = null); 44 | } 45 | return _onDragEnter!.stream; 46 | } 47 | 48 | /// Fired periodically while a [Draggable] is moved over a [Dropzone]. 49 | Stream get onDragOver { 50 | if (_onDragOver == null) { 51 | _onDragOver = StreamController.broadcast( 52 | sync: true, onCancel: () => _onDragOver = null); 53 | } 54 | return _onDragOver!.stream; 55 | } 56 | 57 | /// Fired when a [Draggable] leaves this [Dropzone]. 58 | Stream get onDragLeave { 59 | if (_onDragLeave == null) { 60 | _onDragLeave = StreamController.broadcast( 61 | sync: true, onCancel: () => _onDragLeave = null); 62 | } 63 | return _onDragLeave!.stream; 64 | } 65 | 66 | /// Fired at the end of the drag operation when the [Draggable] is dropped 67 | /// inside this [Dropzone]. 68 | Stream get onDrop { 69 | if (_onDrop == null) { 70 | _onDrop = StreamController.broadcast( 71 | sync: true, onCancel: () => _onDrop = null); 72 | } 73 | return _onDrop!.stream; 74 | } 75 | 76 | // ------------------- 77 | // Private Properties 78 | // ------------------- 79 | /// The list of [Element]s for the dropzone. 80 | late List _elements; 81 | 82 | /// Tracks subscriptions. 83 | List _subs = []; 84 | 85 | /// Creates a [Dropzone] for [elementOrElementList]. The 86 | /// [elementOrElementList] must be of type [Element] or [ElementList]. 87 | /// 88 | /// ## Options 89 | /// 90 | /// The [acceptor] is used to determine which [Draggable]s will be accepted by 91 | /// this [Dropzone]. If none is specified, all [Draggable]s will be accepted. 92 | /// 93 | /// The [overClass] is the css class set to the dropzone element when an 94 | /// accepted [Draggable] is dragged over it. If set to null, no such css class 95 | /// is added. 96 | /// 97 | /// The [invalidClass] is the css class set to the dropzone element when an 98 | /// not-accepted [Draggable] is dragged over it. If set to null, no such css 99 | /// class is added. 100 | Dropzone(elementOrElementList, 101 | {this.acceptor, 102 | this.overClass = 'dnd-over', 103 | this.invalidClass = 'dnd-invalid'}) { 104 | // Wrap in a List if it is not a list but a single Element. 105 | _elements = elementOrElementList is List 106 | ? elementOrElementList 107 | : [elementOrElementList]; 108 | 109 | // Install drag listeners on Elements. 110 | _elements.forEach(_installCustomDragListener); 111 | } 112 | 113 | /// Installs the custom drag listeners (dragEnter, dragOver, dragLeave, and 114 | /// drop) on [element]. 115 | void _installCustomDragListener(Element element) { 116 | _subs.add(_DragEventDispatcher.enterEvent 117 | .forTarget(element) 118 | .listen(_handleDragEnter)); 119 | _subs.add(_DragEventDispatcher.overEvent 120 | .forTarget(element) 121 | .listen(_handleDragOver)); 122 | _subs.add(_DragEventDispatcher.leaveEvent 123 | .forTarget(element) 124 | .listen(_handleDragLeave)); 125 | _subs.add( 126 | _DragEventDispatcher.dropEvent.forTarget(element).listen(_handleDrop)); 127 | } 128 | 129 | /// Handles dragEnter events. 130 | void _handleDragEnter(MouseEvent event) { 131 | // Only handle dragEnter if user moved from outside of element into the 132 | // element. That means we ignore it if user is coming from a child element. 133 | if (event.relatedTarget is Element && 134 | (event.currentTarget as Element) 135 | .contains(event.relatedTarget as Element)) { 136 | return; 137 | } 138 | 139 | // Test if the current draggable is accepted by this dropzone. If there is 140 | // no accepter all are accepted. 141 | if (acceptor == null || 142 | acceptor!.accepts(_currentDrag!.element, _currentDrag!.draggableId, 143 | event.currentTarget as Element)) { 144 | // Fire dragEnter event. 145 | if (_onDragEnter != null) { 146 | _onDragEnter!.add( 147 | DropzoneEvent._(event.currentTarget as Element, _currentDrag!)); 148 | } 149 | 150 | // Add the css class to indicate drag over. 151 | (event.currentTarget as Element).classes.add(overClass); 152 | } else { 153 | // Add the css class to indicate invalid drag over. 154 | (event.currentTarget as Element).classes.add(invalidClass); 155 | } 156 | } 157 | 158 | /// Handles dragOver events. 159 | void _handleDragOver(MouseEvent event) { 160 | // Test if the current draggable is accepted by this dropzone. If there is 161 | // no accepter all are accepted. 162 | if (acceptor == null || 163 | acceptor!.accepts(_currentDrag!.element, _currentDrag!.draggableId, 164 | event.currentTarget as Element)) { 165 | // Fire dragOver event. 166 | if (_onDragOver != null) { 167 | _onDragOver!.add( 168 | DropzoneEvent._(event.currentTarget as Element, _currentDrag!)); 169 | } 170 | } 171 | } 172 | 173 | /// Handles dragLeave events. 174 | void _handleDragLeave(MouseEvent event) { 175 | // Only handle dragLeave if user moved from inside of element to the 176 | // outside. That means we ignore it if user is moving to a child element. 177 | if (event.relatedTarget is Element && 178 | (event.currentTarget as Element) 179 | .contains(event.relatedTarget as Element)) { 180 | return; 181 | } 182 | 183 | // Test if the current draggable is accepted by this dropzone. If there is 184 | // no accepter all are accepted. 185 | if (acceptor == null || 186 | acceptor!.accepts(_currentDrag!.element, _currentDrag!.draggableId, 187 | event.currentTarget as Element)) { 188 | // Fire dragLeave event. 189 | if (_onDragLeave != null) { 190 | _onDragLeave!.add( 191 | DropzoneEvent._(event.currentTarget as Element, _currentDrag!)); 192 | } 193 | 194 | // Remove the css class. 195 | (event.currentTarget as Element).classes.remove(overClass); 196 | } else { 197 | // Remove the invalid drag css class. 198 | (event.currentTarget as Element).classes.remove(invalidClass); 199 | } 200 | } 201 | 202 | /// Handles drop events. 203 | void _handleDrop(MouseEvent event) { 204 | // Test if the current draggable is accepted by this dropzone. If there is 205 | // no accepter all are accepted. 206 | if (acceptor == null || 207 | acceptor!.accepts(_currentDrag!.element, _currentDrag!.draggableId, 208 | event.currentTarget as Element)) { 209 | // Fire drop event. 210 | if (_onDrop != null) { 211 | _onDrop!.add( 212 | DropzoneEvent._(event.currentTarget as Element, _currentDrag!)); 213 | } 214 | } 215 | } 216 | 217 | /// Unistalls all listeners. 218 | void destroy() { 219 | _subs.forEach((sub) => sub.cancel()); 220 | _subs.clear(); 221 | } 222 | } 223 | 224 | /// Event for dropzone elements. 225 | class DropzoneEvent { 226 | /// The [Element] of the [Dropzone]. 227 | final Element dropzoneElement; 228 | 229 | /// The [Element] that is beeing dragged. 230 | final Element draggableElement; 231 | 232 | /// The [AvatarHandler] or null if there is none. 233 | final AvatarHandler? avatarHandler; 234 | 235 | /// The current mouse/touch position, relative to the whole document (page 236 | /// position). 237 | final Point position; 238 | 239 | DropzoneEvent._(this.dropzoneElement, _DragInfo dragInfo) 240 | : draggableElement = dragInfo.element, 241 | avatarHandler = dragInfo.avatarHandler, 242 | position = dragInfo.position; 243 | } 244 | -------------------------------------------------------------------------------- /lib/src/dropzone_acceptor.dart: -------------------------------------------------------------------------------- 1 | part of dnd; 2 | 3 | /// An acceptor defines which draggable elements are accepted by a [Dropzone]. 4 | abstract class Acceptor { 5 | Acceptor(); 6 | 7 | /// Creates an [Acceptor] that accepts all drag elements that are part of the 8 | /// specified [draggables]. 9 | /// 10 | /// See [DraggablesAcceptor]. 11 | factory Acceptor.draggables(List draggables) { 12 | return DraggablesAcceptor(draggables); 13 | } 14 | 15 | /// Returns true if the [draggableElement] with [draggableId] should be 16 | /// accepted by the [dropzoneElement]. 17 | bool accepts( 18 | Element draggableElement, int draggableId, Element dropzoneElement); 19 | } 20 | 21 | /// The [DraggablesAcceptor] accepts all drag elements that are part of the 22 | /// specified list of [Draggable]s. 23 | class DraggablesAcceptor extends Acceptor { 24 | final Set draggableIds = Set(); 25 | 26 | DraggablesAcceptor(List draggables) { 27 | draggables.forEach((d) => draggableIds.add(d.id)); 28 | } 29 | 30 | @override 31 | bool accepts( 32 | Element draggableElement, int draggableId, Element dropzoneElement) { 33 | return draggableIds.contains(draggableId); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dnd 2 | version: 2.0.1 3 | author: Marco Jakob 4 | description: Drag and Drop for Dart web apps with mouse and touch support. 5 | repository: https://github.com/marcojakob/dart-dnd 6 | homepage: https://code.makery.ch/library/dart-drag-and-drop/ 7 | environment: 8 | sdk: '>=2.12.0 <3.0.0' 9 | dev_dependencies: 10 | build_runner: ^1.2.5 11 | build_web_compilers: ^2.16.4 12 | --------------------------------------------------------------------------------