├── README.md ├── bower.json ├── dragon-drop.js └── example.html /README.md: -------------------------------------------------------------------------------- 1 | # angular-dragon-drop 2 | "Drag and drop" directives for AngularJS. Work in progress. 3 | 4 | ## Install 5 | 6 | ```shell 7 | bower install angular-dragon-drop 8 | ``` 9 | 10 | ## Usage 11 | 1. Include the `dragon-drop.js` script provided by this component into your app. 12 | 2. Add `btford.dragon-drop` as a module dependency to your app. 13 | 14 | Repeats a template inside the dragon over a list. 15 | ```html 16 |
17 | {{item.name}} 18 |
19 |
20 | {{item.name}} 21 |
22 | ``` 23 | You can drag from one dragon onto another, and the models will be updated accordingly. 24 | 25 | It also works on objects: 26 | ```html 27 |
28 | {{key}}: {{value}} 29 |
30 |
31 | {{key}}: {{value}} 32 |
33 | ``` 34 | 35 | 36 | ## Config 37 | This is not a kitchen sink every-option-you-can-think-of module. 38 | This is a starting point. 39 | Configure by forking and editing the code according to your needs. 40 | Send a PR if you think your additions are widely useful. :) 41 | 42 | ### btf-double-dragon 43 | Instead of removing values from the array this dragon is bound to, the values are duplicated. 44 | Add the `btf-double-dragon` attribute to an element with the `btf-dragon` attribute to get the behavior. 45 | 46 | Example: 47 | ```html 48 |

These get copied

49 |
50 | {{item.name}} 51 |
52 |

These get moved

53 |
54 | {{item.name}} 55 |
56 | ``` 57 | 58 | ### btf-dragon-accepts 59 | Makes the dragon only accepts items that pass the truth test function given by this argument. 60 | Add the `btf-dragon-accepts` attribute to an element to get the behavior. 61 | 62 | Example: 63 | ```html 64 |

You can only put shiny objects here

h2> 65 |
66 | {{item.name}} 67 |
68 |

This takes anything

69 |
70 | {{item.name}} 71 |
72 | ``` 73 | 74 | ```javascript 75 | // in a Ctrl... 76 | $scope.shinyThings = function (item) { 77 | return !!item.shiny; 78 | }; 79 | ``` 80 | 81 | ## Example 82 | See [`example.html`](http://htmlpreview.github.io/?https://github.com/btford/angular-dragon-drop/blob/master/example.html). 83 | 84 | ## License 85 | MIT 86 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-dragon-drop", 3 | "version": "0.3.1", 4 | "main": "dragon-drop.js", 5 | "dependencies": { 6 | "angular": "~1.0.5" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /dragon-drop.js: -------------------------------------------------------------------------------- 1 | /* 2 | * angular-dragon-drop v0.3.1 3 | * (c) 2013 Brian Ford http://briantford.com 4 | * License: MIT 5 | */ 6 | 7 | 'use strict'; 8 | 9 | angular.module('btford.dragon-drop', []). 10 | directive('btfDragon', function ($document, $compile, $rootScope) { 11 | /* 12 | ^ ^ 13 | |\ \ / /| 14 | / \ |\__ __/| / \ 15 | / /\ \ \ _ \/ _ / / \ 16 | / / /\ \ {*}\/{*} / / \ \ 17 | | | | \ \( (00) ) / // |\ \ 18 | | | | |\ \(V""V)\ / / | || \| 19 | | | | | \ |^--^| \ / / || || || 20 | / / / | |( WWWW__ \/ /| || || || 21 | | | | | | | \______\ / / || || || 22 | | | | / | | )|______\ ) | / | || || 23 | / / / / / /______/ /| \ \ || || 24 | / / / / / /\_____/ |/ /__\ \ \ \ \ 25 | | | | / / /\______/ \ \__| \ \ \ 26 | | | | | | |\______ __ \_ \__|_| \ 27 | | | ,___ /\______ _ _ \_ \ | 28 | | |/ /\_____ / \ \__ \ | /\ 29 | |/ | |\______ | | \___ \ |__/ \ 30 | v | |\______ | | \___/ | 31 | | |\______ | | __/ 32 | \ \________\_ _\ ____/ 33 | __/ /\_____ __/ / )\_, _____/ 34 | / ___/ \uuuu/ ___/___) \______/ 35 | VVV V VVV V 36 | */ 37 | // this ASCII dragon is really important, do not remove 38 | 39 | var dragValue, 40 | dragKey, 41 | dragOrigin, 42 | dragDuplicate = false, 43 | floaty, 44 | offsetX, 45 | offsetY; 46 | 47 | var drag = function (ev) { 48 | var x = ev.clientX - offsetX, 49 | y = ev.clientY - offsetY; 50 | 51 | floaty.css('left', x + 'px'); 52 | floaty.css('top', y + 'px'); 53 | }; 54 | 55 | var remove = function (collection, index) { 56 | if (collection instanceof Array) { 57 | return collection.splice(index, 1); 58 | } else { 59 | var temp = collection[index]; 60 | delete collection[index]; 61 | return temp; 62 | } 63 | }; 64 | 65 | var add = function (collection, item, key) { 66 | if (collection instanceof Array) { 67 | collection.push(item); 68 | } else { 69 | collection[key] = item; 70 | } 71 | }; 72 | 73 | var documentBody = angular.element($document[0].body); 74 | 75 | var disableSelect = function () { 76 | documentBody.css({ 77 | '-moz-user-select': '-moz-none', 78 | '-khtml-user-select': 'none', 79 | '-webkit-user-select': 'none', 80 | '-ms-user-select': 'none', 81 | 'user-select': 'none' 82 | }); 83 | }; 84 | 85 | var enableSelect = function () { 86 | documentBody.css({ 87 | '-moz-user-select': '', 88 | '-khtml-user-select': '', 89 | '-webkit-user-select': '', 90 | '-ms-user-select': '', 91 | 'user-select': '' 92 | }); 93 | }; 94 | 95 | var killFloaty = function () { 96 | if (floaty) { 97 | $document.unbind('mousemove', drag); 98 | floaty.remove(); 99 | floaty = null; 100 | } 101 | }; 102 | 103 | var getElementOffset = function (elt) { 104 | 105 | var box = elt.getBoundingClientRect(); 106 | var body = $document[0].body; 107 | 108 | var xPosition = box.left + body.scrollLeft; 109 | var yPosition = box.top + body.scrollTop; 110 | 111 | return { 112 | left: xPosition, 113 | top: yPosition 114 | }; 115 | }; 116 | 117 | // Get the element at position (`x`, `y`) behind the given element 118 | var getElementBehindPoint = function (behind, x, y) { 119 | var originalDisplay = behind.css('display'); 120 | behind.css('display', 'none'); 121 | 122 | var element = angular.element($document[0].elementFromPoint(x, y)); 123 | 124 | behind.css('display', originalDisplay); 125 | 126 | return element; 127 | }; 128 | 129 | $document.bind('mouseup', function (ev) { 130 | if (!dragValue) { 131 | return; 132 | } 133 | 134 | var dropArea = getElementBehindPoint(floaty, ev.clientX, ev.clientY); 135 | 136 | var accepts = function () { 137 | return dropArea.attr('btf-dragon') && 138 | ( !dropArea.attr('btf-dragon-accepts') || 139 | dropArea.scope().$eval(dropArea.attr('btf-dragon-accepts'))(dragValue) ); 140 | }; 141 | 142 | while (dropArea.length > 0 && !accepts()) { 143 | dropArea = dropArea.parent(); 144 | } 145 | 146 | if (dropArea.length > 0) { 147 | var expression = dropArea.attr('btf-dragon'); 148 | var targetScope = dropArea.scope(); 149 | var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*$/); 150 | 151 | var targetList = targetScope.$eval(match[2]); 152 | targetScope.$apply(function () { 153 | add(targetList, dragValue, dragKey); 154 | }); 155 | } else if (!dragDuplicate) { 156 | // no dropArea here 157 | // put item back to origin 158 | $rootScope.$apply(function () { 159 | add(dragOrigin, dragValue, dragKey); 160 | }); 161 | } 162 | 163 | dragValue = dragOrigin = null; 164 | killFloaty(); 165 | }); 166 | 167 | return { 168 | restrict: 'A', 169 | 170 | compile: function (container, attr) { 171 | 172 | // get the `thing in things` expression 173 | var expression = attr.btfDragon; 174 | var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*$/); 175 | if (!match) { 176 | throw Error("Expected btfDragon in form of '_item_ in _collection_' but got '" + 177 | expression + "'."); 178 | } 179 | var lhs = match[1]; 180 | var rhs = match[2]; 181 | 182 | match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); 183 | 184 | var valueIdentifier = match[3] || match[1]; 185 | var keyIdentifier = match[2]; 186 | 187 | // pull out the template to re-use. 188 | // Improvised ng-transclude. 189 | var template = container.html(); 190 | 191 | // wrap text nodes 192 | try { 193 | template = angular.element(template.trim()); 194 | if (template.length === 0) { 195 | throw new Error(''); 196 | } 197 | } 198 | catch (e) { 199 | template = angular.element('' + template + ''); 200 | } 201 | var child = template.clone(); 202 | child.attr('ng-repeat', expression); 203 | 204 | container.html(''); 205 | container.append(child); 206 | 207 | var duplicate = container.attr('btf-double-dragon') !== undefined; 208 | 209 | return function (scope, elt, attr) { 210 | 211 | var accepts = scope.$eval(attr.btfDragonAccepts); 212 | 213 | if (accepts !== undefined && typeof accepts !== 'function') { 214 | throw Error('Expected btfDragonAccepts to be a function.'); 215 | } 216 | 217 | var spawnFloaty = function () { 218 | scope.$apply(function () { 219 | floaty = template.clone(); 220 | floaty.css('position', 'fixed'); 221 | 222 | floaty.css('margin', '0px'); 223 | floaty.css('z-index', '99999'); 224 | 225 | var floatyScope = scope.$new(); 226 | floatyScope[valueIdentifier] = dragValue; 227 | if (keyIdentifier) { 228 | floatyScope[keyIdentifier] = dragKey; 229 | } 230 | $compile(floaty)(floatyScope); 231 | documentBody.append(floaty); 232 | $document.bind('mousemove', drag); 233 | disableSelect(); 234 | }); 235 | }; 236 | 237 | elt.bind('mousedown', function (ev) { 238 | if (dragValue) { 239 | return; 240 | } 241 | 242 | // find the right parent 243 | var originElement = angular.element(ev.target); 244 | var originScope = originElement.scope(); 245 | 246 | while (originScope[valueIdentifier] === undefined) { 247 | originScope = originScope.$parent; 248 | if (!originScope) { 249 | return; 250 | } 251 | } 252 | 253 | dragValue = originScope[valueIdentifier]; 254 | dragKey = originScope[keyIdentifier]; 255 | if (!dragValue) { 256 | return; 257 | } 258 | 259 | // get offset inside element to drag 260 | var offset = getElementOffset(ev.target); 261 | 262 | dragOrigin = scope.$eval(rhs); 263 | if (duplicate) { 264 | dragValue = angular.copy(dragValue); 265 | } else { 266 | scope.$apply(function () { 267 | remove(dragOrigin, dragKey || dragOrigin.indexOf(dragValue)); 268 | }); 269 | } 270 | dragDuplicate = duplicate; 271 | 272 | offsetX = (ev.pageX - offset.left); 273 | offsetY = (ev.pageY - offset.top); 274 | 275 | spawnFloaty(); 276 | drag(ev); 277 | }); 278 | }; 279 | } 280 | }; 281 | }); 282 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dragon Drop for AngularJS 5 | 6 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |

Dragon Drop Example

19 |
20 | 21 |
22 | 23 |
24 |
25 |

Things

26 |
{{thing}}
27 |
28 |
29 |

Other Things

30 |
{{thing}}
31 |
32 |
33 | 34 |
35 | 36 |
37 |
38 |

Things

39 |
{{things | json}}
40 |
41 |
42 |

Other Things

43 |
{{otherThings | json}}
44 |
45 |
46 | 47 |
48 | 49 | 50 | 51 | 58 | 59 | --------------------------------------------------------------------------------