├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── build.js ├── dist ├── angular-drag-drop.js └── angular-drag-drop.min.js ├── example ├── build │ └── dragular.js ├── index.html ├── src │ ├── controllers │ │ └── game.js │ ├── dragular.css │ ├── dragular.js │ └── services │ │ └── board.js └── webpack.config.js ├── package.json ├── src ├── .eslintrc.js └── angular-drag-drop.js └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | ], 5 | parserOptions: { 6 | ecmaVersion: 6, 7 | }, 8 | env: { 9 | node: true, 10 | }, 11 | rules: { 12 | } 13 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | *.swo 11 | .DS_Store 12 | 13 | node_modules 14 | 15 | .vscode 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014-2015 the Geoffrey Goodman, https://github.com/ggoodman 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Angular drag-and-drop 2 | ===================== 3 | 4 | [![Join the chat at https://gitter.im/ggoodman/angular-drag-drop](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ggoodman/angular-drag-drop?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | Declarative drag and drop with zero dependencies in Angular.js 7 | 8 | Copyright (C) 2015, Geoff Goodman (https://github.com/ggoodman) 9 | 10 | 11 | 12 | Installation 13 | ------------ 14 | 15 | Several installation options: 16 | * npm: `npm install --save angular-drag-drop` 17 | * Download from github: [angular-drag-drop.min.js](https://raw.github.com/ggoodman/angular-drag-drop/master/dist/angular-drag-drop.min.js) 18 | 19 | 20 | 21 | Demo 22 | ---- 23 | 24 | [Dragular](http://bit.ly/17E25d2) - Drag tiles around like the famous 25 | [Sliding Puzzle](http://en.wikipedia.org/wiki/Sliding_puzzle) using angular drag and drop. 26 | 27 | Usage 28 | ----- 29 | 30 | Using Webpack or Browserify: 31 | 32 | ```js 33 | var Angular = require('angular'); 34 | 35 | var mod = Angular.module('yourModule', [ 36 | require('angular-drag-drop'), 37 | ]); 38 | 39 | // Now use `drag-container`, `drop-container` and `drop-target` in your templates 40 | ``` 41 | 42 | 43 | 44 | ### `drag-container` 45 | 46 | Define a DOM element that will become draggable and determines what the data associated with the drag event is. 47 | 48 | **Example** 49 | 50 | ```html 51 |
56 | ``` 57 | 58 | Attribute | Required? | Description 59 | ----------|-----------|------------ 60 | `drag-container` | Yes | Defines when the drag action is allowed or blocked for the draggable. Can be true or false. 61 | `drag-data` | No | Bind the data to be associated with dragging this element. When not specified the jqLite element on which the directive is placed will be used as the $dragData. 62 | 63 | The following callbacks are optional. 64 | Each can allow you to inject two special objects, `$event` and `$dragData`. 65 | `$event` is the original browser event. 66 | This can be helpful for setting the browser-level drag data using `$event.dataTransfer.setData('mime/type', data)`) 67 | or for setting the drag image / drop effect like `$event.dataTransfer.dropEffect = 'copy'`. 68 | `$dragData` is the data associated with dragging this element. 69 | It is optionally set by providing a reference via the `drag-container` attribute. 70 | 71 | * `on-drag-start` 72 | * `on-drag-end` 73 | 74 | 75 | 76 | ### `drop-container` 77 | 78 | Define a DOM element that will accept draggable elements that match pass an optional acceptance callback. 79 | 80 | **Example** 81 | 82 | ```html 83 |
90 | ``` 91 | 92 | Attribute | Required? | Description 93 | ----------|-----------|------------ 94 | `drop-accepts` | No | Define a call to check if the data being dragged is allowed 95 | 96 | The following callbacks are optional. 97 | Each can allow you to inject two special objects, `$event` and `$dragData`. 98 | `$event` is the original browser event. 99 | `$dragData` is the data associated with dragging this element. 100 | 101 | * `on-drag-enter` 102 | * `on-drag-over` 103 | * `on-drag-leave` 104 | * `on-drop` 105 | 106 | 107 | 108 | ### `drop-target` 109 | 110 | Define a region of the parent `drop-container` that can independently accept drag and drop events in a logical region. 111 | 112 | This module will only consider those logical regions that have `drop-targets` bound in determining which region 113 | should receive events at any point in time. The algorithm to determine which logical region is active is based 114 | on the proximity of the cursor to the virtual center-point of each logical region. 115 | 116 | **Must be a child of a `drop-container`** 117 | 118 | **Example** 119 | 120 | ```html 121 |
127 | ``` 128 | 129 | Attribute | Required? | Description 130 | ----------|-----------|------------ 131 | `drop-target` | Yes | Defines the logical region of the parent `drop-container` that will accept events. Can be one of: `center`, `top`, `top-right`, `right`, `bottom-right`, `bottom`, `bottom-left`, `left`, `top-left` 132 | 133 | The following callbacks are optional. 134 | Each can allow you to inject two special objects, `$event` and `$dragData`. 135 | `$event` is the original browser event. 136 | `$dragData` is the data associated with dragging this element. 137 | 138 | * `on-drag-enter` 139 | * `on-drag-over` 140 | * `on-drag-leave` 141 | * `on-drop` 142 | 143 | 144 | 145 | License 146 | ------- 147 | 148 | Released under the terms of MIT License: 149 | 150 | Permission is hereby granted, free of charge, to any person obtaining 151 | a copy of this software and associated documentation files (the 152 | 'Software'), to deal in the Software without restriction, including 153 | without limitation the rights to use, copy, modify, merge, publish, 154 | distribute, sublicense, and/or sell copies of the Software, and to 155 | permit persons to whom the Software is furnished to do so, subject to 156 | the following conditions: 157 | 158 | The above copyright notice and this permission notice shall be 159 | included in all copies or substantial portions of the Software. 160 | 161 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 162 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 163 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 164 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 165 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 166 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 167 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 168 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | var Config = require("./webpack.config"); 2 | var Package = require("./package.json"); 3 | var Webpack = require("webpack"); 4 | 5 | var buildUnminified = function (config, callback) { 6 | config.plugins = []; 7 | config.output.filename = Package.name + ".js"; 8 | 9 | Webpack(config, callback); 10 | }; 11 | 12 | var buildMinified = function (config, callback) { 13 | config.plugins = [new Webpack.optimize.UglifyJsPlugin()]; 14 | config.output.filename = Package.name + ".min.js"; 15 | 16 | Webpack(config, callback); 17 | }; 18 | 19 | buildUnminified(Config, function (err, stats) { 20 | if (err) { 21 | console.error("[ERR] Build failed: "); 22 | console.trace(err); 23 | 24 | return; 25 | } 26 | 27 | console.log("[OK] Unminified built: " + stats.toString({colors: true})); 28 | 29 | buildMinified(Config, function (err, stats) { 30 | if (err) { 31 | console.error("[ERR] Build failed: "); 32 | console.trace(err); 33 | 34 | return; 35 | } 36 | 37 | console.log("[OK] Minified built: " + stats.toString({colors: true})); 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /dist/angular-drag-drop.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("angular")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["angular"], factory); 6 | else if(typeof exports === 'object') 7 | exports["AngularDragDrop"] = factory(require("angular")); 8 | else 9 | root["AngularDragDrop"] = factory(root["angular"]); 10 | })(this, function(__WEBPACK_EXTERNAL_MODULE_1__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | 39 | 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports, __webpack_require__) { 56 | 57 | var Angular = __webpack_require__(1); 58 | 59 | module.exports = 'filearts.dragDrop'; 60 | 61 | var mod = Angular.module(module.exports, []); 62 | var stylesheet = 63 | '.drag-active .drop-container{position:relative}.drag-active .drop-container *{pointer-events:none}.drag-active .drop-container:before{position:absolute;top:0;right:0;bottom:0;left:0;z-index:9999;content:""}'; 64 | 65 | mod.factory('dragContext', [ 66 | function() { 67 | var context = {}; 68 | 69 | return reset(); 70 | 71 | function reset() { 72 | return Angular.extend(context, { 73 | data: null, 74 | reset: reset, 75 | start: start, 76 | }); 77 | } 78 | 79 | function start(data) { 80 | context.data = data; 81 | 82 | return data; 83 | } 84 | }, 85 | ]); 86 | 87 | mod.run([ 88 | '$document', 89 | '$rootElement', 90 | '$timeout', 91 | function($document, $rootElement, $timeout) { 92 | $document[0].addEventListener('dragend', onDragEnd, true); 93 | $document[0].addEventListener('drop', onDrop, true); 94 | 95 | loadStyles(stylesheet, $document[0]); 96 | 97 | function onDragEnd() { 98 | clearDragActive(); 99 | } 100 | 101 | function onDrop() { 102 | clearDragActive(); 103 | } 104 | 105 | function clearDragActive() { 106 | $timeout(function() { 107 | var target = findDragActiveTarget($rootElement); 108 | 109 | target.removeClass('drag-active'); 110 | }); 111 | } 112 | 113 | /** 114 | * Load styles into the head element 115 | * 116 | * Source: https://github.com/webmodules/load-styles/blob/master/index.js 117 | */ 118 | function loadStyles(css, doc) { 119 | // default to the global `document` object 120 | if (!doc) doc = document; 121 | 122 | var head = doc.head || doc.getElementsByTagName('head')[0]; 123 | 124 | // no node? create one... 125 | if (!head) { 126 | head = doc.createElement('head'); 127 | var body = doc.body || doc.getElementsByTagName('body')[0]; 128 | if (body) { 129 | body.parentNode.insertBefore(head, body); 130 | } else { 131 | doc.documentElement.appendChild(head); 132 | } 133 | } 134 | 135 | var style = doc.createElement('style'); 136 | style.type = 'text/css'; 137 | if (style.styleSheet) { 138 | // IE 139 | style.styleSheet.cssText = css; 140 | } else { 141 | // the world 142 | style.appendChild(doc.createTextNode(css)); 143 | } 144 | head.appendChild(style); 145 | 146 | return style; 147 | } 148 | }, 149 | ]); 150 | 151 | mod.directive('dragContainer', [ 152 | '$rootElement', 153 | '$parse', 154 | '$timeout', 155 | 'dragContext', 156 | function($rootElement, $parse, $timeout, dragContext) { 157 | return { 158 | restrict: 'A', 159 | link: function($scope, $element, $attrs) { 160 | var onDragStart = $attrs.onDragStart 161 | ? $parse($attrs.onDragStart) 162 | : null; 163 | var onDragEnd = $attrs.onDragEnd 164 | ? $parse($attrs.onDragEnd) 165 | : null; 166 | 167 | $attrs.$addClass('drag-container'); 168 | 169 | $scope.$watch($attrs.dragContainer, function(draggable) { 170 | $attrs.$set( 171 | 'draggable', 172 | typeof draggable === 'undefined' || draggable 173 | ); 174 | }); 175 | 176 | $element.on('dragstart', handleDragStart); 177 | $element.on('dragend', handleDragEnd); 178 | 179 | function handleDragStart(e) { 180 | $timeout( 181 | function() { 182 | var target = findDragActiveTarget($rootElement); 183 | 184 | target.addClass('drag-active'); 185 | }, 186 | 0, 187 | false 188 | ); 189 | 190 | dragContext.start( 191 | $attrs.dragData 192 | ? $scope.$eval($attrs.dragData) 193 | : $element 194 | ); 195 | $element.addClass('drag-container-active'); 196 | 197 | if (onDragStart) { 198 | var locals = { 199 | $event: e, 200 | $dragData: dragContext.data, 201 | }; 202 | 203 | $scope.$apply(function() { 204 | onDragStart($scope, locals); 205 | }); 206 | } 207 | 208 | var targetEvent = e.originalEvent || e; 209 | 210 | if (targetEvent.dataTransfer) { 211 | if ( 212 | (!targetEvent.dataTransfer.items || 213 | !targetEvent.dataTransfer.items.length) && 214 | (!targetEvent.dataTransfer.files || 215 | !targetEvent.dataTransfer.files.length) 216 | ) { 217 | targetEvent.dataTransfer.setData('text', ''); 218 | } 219 | } 220 | } 221 | 222 | function handleDragEnd(e) { 223 | $timeout( 224 | function() { 225 | var target = findDragActiveTarget($rootElement); 226 | 227 | target.removeClass('drag-active'); 228 | }, 229 | 0, 230 | false 231 | ); 232 | 233 | dragContext.reset(); 234 | $element.removeClass('drag-container-active'); 235 | 236 | if (onDragEnd) { 237 | var locals = { 238 | $event: e, 239 | $dragData: dragContext.data, 240 | }; 241 | 242 | $scope.$apply(function() { 243 | onDragEnd($scope, locals); 244 | }); 245 | } 246 | 247 | if (dragContext.lastTarget) { 248 | dragContext.lastTarget.$attrs.$removeClass('drag-over'); 249 | } 250 | } 251 | }, 252 | }; 253 | }, 254 | ]); 255 | 256 | mod.directive('dropContainer', [ 257 | '$document', 258 | '$parse', 259 | '$window', 260 | 'dragContext', 261 | function($document, $parse, $window, dragContext) { 262 | return { 263 | restrict: 'A', 264 | require: 'dropContainer', 265 | controller: 'DropContainerController', 266 | controllerAs: 'dropContainer', 267 | link: function($scope, $element, $attrs, dropContainer) { 268 | var acceptsFn = $attrs.dropAccepts 269 | ? $parse($attrs.dropAccepts) 270 | : function($scope, locals) { 271 | return typeof locals.$dragData !== 'undefined'; 272 | }; 273 | var onDragEnter = $attrs.onDragEnter 274 | ? $parse($attrs.onDragEnter) 275 | : null; 276 | var onDragOver = $attrs.onDragOver 277 | ? $parse($attrs.onDragOver) 278 | : null; 279 | var onDragLeave = $attrs.onDragLeave 280 | ? $parse($attrs.onDragLeave) 281 | : null; 282 | var onDrop = $attrs.onDrop ? $parse($attrs.onDrop) : null; 283 | 284 | $attrs.$addClass('drop-container'); 285 | 286 | $element.on('dragover', handleDragOver); 287 | $element.on('dragenter', handleDragEnter); 288 | $element.on('dragleave', handleDragLeave); 289 | $element.on('drop', handleDrop); 290 | 291 | function handleDragEnter(e) { 292 | if ( 293 | dragContext.lastTarget && 294 | dragContext.lastTarget !== $element 295 | ) { 296 | dragContext.lastTarget.$attrs.$removeClass('drag-over'); 297 | } 298 | 299 | dragContext.lastTarget = { 300 | $attrs: $attrs, 301 | $element: $element, 302 | }; 303 | 304 | var locals = { 305 | $event: e, 306 | $dragData: dragContext.data, 307 | }; 308 | 309 | if (acceptsFn($scope, locals)) { 310 | e.preventDefault(); 311 | 312 | $attrs.$addClass('drag-over'); 313 | 314 | if (onDragEnter) { 315 | $scope.$apply(function() { 316 | onDragEnter($scope, locals); 317 | }); 318 | } 319 | } 320 | } 321 | 322 | function handleDragOver(e) { 323 | var locals = { 324 | $event: e, 325 | $dragData: dragContext.data, 326 | }; 327 | 328 | if (acceptsFn($scope, locals)) { 329 | e.preventDefault(); 330 | 331 | var pos = offset($element); 332 | 333 | $attrs.$addClass('drag-over'); 334 | 335 | var minDistanceSq = Number.MAX_VALUE; 336 | var width = pos.width; 337 | var height = pos.height; 338 | var x = e.pageX - pos.left; 339 | var y = e.pageY - pos.top; 340 | var closestTarget = dropContainer.lastTarget; 341 | 342 | Angular.forEach(dropContainer.targets, function( 343 | dropTarget, 344 | anchor 345 | ) { 346 | var anchorX = width / 2; 347 | var anchorY = height / 2; 348 | 349 | if (anchor.indexOf('left') >= 0) 350 | anchorX = width * 1 / 4; 351 | if (anchor.indexOf('top') >= 0) 352 | anchorY = height * 1 / 4; 353 | if (anchor.indexOf('right') >= 0) 354 | anchorX = width * 3 / 4; 355 | if (anchor.indexOf('bottom') >= 0) 356 | anchorY = height * 3 / 4; 357 | 358 | var distanceSq = 359 | Math.pow(anchorX - x, 2) + 360 | Math.pow(anchorY - y, 2); 361 | 362 | if (distanceSq < minDistanceSq) { 363 | closestTarget = dropTarget; 364 | minDistanceSq = distanceSq; 365 | } 366 | }); 367 | 368 | $scope.$apply(function() { 369 | if (onDragOver) { 370 | onDragOver($scope, locals); 371 | } 372 | 373 | if (!closestTarget) return; 374 | 375 | if (closestTarget !== dropContainer.lastTarget) { 376 | if (dropContainer.lastTarget) { 377 | $attrs.$removeClass( 378 | 'drop-container-' + 379 | dropContainer.lastTarget.anchor 380 | ); 381 | } 382 | 383 | $attrs.$addClass( 384 | 'drop-container-' + closestTarget.anchor 385 | ); 386 | 387 | if (dropContainer.lastTarget) { 388 | dropContainer.lastTarget.handleDragLeave( 389 | e, 390 | locals 391 | ); 392 | } 393 | 394 | closestTarget.handleDragEnter(e, locals); 395 | 396 | dropContainer.lastTarget = closestTarget; 397 | } 398 | 399 | closestTarget.handleDragOver(e); 400 | }); 401 | } 402 | } 403 | 404 | function handleDragLeave(e) { 405 | $attrs.$removeClass('drag-over'); 406 | 407 | var locals = { 408 | $event: e, 409 | $dragData: dragContext.data, 410 | }; 411 | 412 | $scope.$apply(function() { 413 | if (onDragLeave) { 414 | onDragLeave($scope, locals); 415 | } 416 | 417 | if (dropContainer.lastTarget) { 418 | dropContainer.lastTarget.handleDragLeave(e, locals); 419 | } 420 | 421 | if (dropContainer.lastTarget) { 422 | $attrs.$removeClass( 423 | 'drop-container-' + 424 | dropContainer.lastTarget.anchor 425 | ); 426 | 427 | dropContainer.lastTarget = null; 428 | } 429 | }); 430 | } 431 | 432 | function handleDrop(e) { 433 | if (dragContext.lastTarget) { 434 | dragContext.lastTarget.$attrs.$removeClass('drag-over'); 435 | } 436 | 437 | var locals = { 438 | $event: e, 439 | $dragData: dragContext.data, 440 | }; 441 | 442 | if (acceptsFn($scope, locals)) { 443 | e.preventDefault(); 444 | dragContext.reset(); 445 | 446 | $scope.$apply(function() { 447 | if (onDrop) { 448 | onDrop($scope, locals); 449 | } 450 | 451 | if (dropContainer.lastTarget) { 452 | dropContainer.lastTarget.handleDrop(e, locals); 453 | } 454 | }); 455 | } 456 | 457 | if (dropContainer.lastTarget) { 458 | $attrs.$removeClass( 459 | 'drop-container-' + dropContainer.lastTarget.anchor 460 | ); 461 | } 462 | 463 | dropContainer.lastTarget = null; 464 | } 465 | }, 466 | }; 467 | 468 | // Source: https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js 469 | function getRawNode(elem) { 470 | return elem[0] || elem; 471 | } 472 | 473 | // Source: https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js 474 | function offset(elem) { 475 | elem = getRawNode(elem); 476 | 477 | var elemBCR = elem.getBoundingClientRect(); 478 | return { 479 | width: Math.round( 480 | Angular.isNumber(elemBCR.width) 481 | ? elemBCR.width 482 | : elem.offsetWidth 483 | ), 484 | height: Math.round( 485 | Angular.isNumber(elemBCR.height) 486 | ? elemBCR.height 487 | : elem.offsetHeight 488 | ), 489 | top: Math.round( 490 | elemBCR.top + 491 | ($window.pageYOffset || 492 | $document[0].documentElement.scrollTop) 493 | ), 494 | left: Math.round( 495 | elemBCR.left + 496 | ($window.pageXOffset || 497 | $document[0].documentElement.scrollLeft) 498 | ), 499 | }; 500 | } 501 | }, 502 | ]); 503 | 504 | mod.controller('DropContainerController', [ 505 | function() { 506 | var dropContainer = this; 507 | var validAnchors = 'center top top-right right bottom-right bottom bottom-left left top-left'.split( 508 | ' ' 509 | ); 510 | 511 | dropContainer.targets = {}; 512 | dropContainer.lastTarget = null; 513 | 514 | dropContainer.attach = function(dropTarget) { 515 | var anchor = dropTarget.anchor; 516 | 517 | if (validAnchors.indexOf(anchor) < 0) { 518 | throw new Error('Invalid drop target anchor `' + anchor + '`.'); 519 | } 520 | 521 | dropContainer.targets[anchor] = dropTarget; 522 | 523 | return dropTarget; 524 | }; 525 | 526 | dropContainer.detach = function(dropTarget) { 527 | var anchor = dropTarget.anchor; 528 | 529 | if (validAnchors.indexOf(anchor) < 0) { 530 | throw new Error('Invalid drop target anchor `' + anchor + '`.'); 531 | } 532 | 533 | if (!dropContainer.targets[anchor] === dropTarget) { 534 | throw new Error( 535 | 'The indicated drop target is not attached at ' + 536 | 'the anchor `' + 537 | anchor + 538 | '`.' 539 | ); 540 | } 541 | 542 | delete dropContainer.targets[anchor]; 543 | 544 | return dropTarget; 545 | }; 546 | }, 547 | ]); 548 | 549 | mod.directive('dropTarget', [ 550 | '$parse', 551 | function($parse) { 552 | return { 553 | restrict: 'A', 554 | require: ['^dropContainer', 'dropTarget'], 555 | scope: true, 556 | bindToController: { 557 | anchor: '@dropTarget', 558 | }, 559 | controller: Angular.noop, 560 | controllerAs: 'dropTarget', 561 | link: function($scope, $element, $attrs, ctls) { 562 | var dropContainer = ctls[0]; 563 | var dropTarget = ctls[1]; 564 | 565 | $attrs.$addClass('drop-target'); 566 | 567 | dropTarget.$attrs = $attrs; 568 | dropTarget.$scope = $scope; 569 | 570 | $attrs.$addClass( 571 | 'drop-target drop-target-' + dropTarget.anchor 572 | ); 573 | 574 | dropContainer.attach(dropTarget); 575 | 576 | var onDragEnter = dropTarget.$attrs.onDragEnter 577 | ? $parse(dropTarget.$attrs.onDragEnter) 578 | : Angular.noop; 579 | var onDragLeave = dropTarget.$attrs.onDragLeave 580 | ? $parse(dropTarget.$attrs.onDragLeave) 581 | : Angular.noop; 582 | var onDragOver = dropTarget.$attrs.onDragOver 583 | ? $parse(dropTarget.$attrs.onDragOver) 584 | : Angular.noop; 585 | var onDrop = dropTarget.$attrs.onDrop 586 | ? $parse(dropTarget.$attrs.onDrop) 587 | : Angular.noop; 588 | 589 | dropTarget.handleDragEnter = function(e, locals) { 590 | onDragEnter(dropTarget.$scope, locals); 591 | }; 592 | 593 | dropTarget.handleDragLeave = function(e, locals) { 594 | onDragLeave(dropTarget.$scope, locals); 595 | }; 596 | 597 | dropTarget.handleDragOver = function(e, locals) { 598 | onDragOver(dropTarget.$scope, locals); 599 | }; 600 | 601 | dropTarget.handleDrop = function(e, locals) { 602 | onDrop(dropTarget.$scope, locals); 603 | }; 604 | 605 | $scope.$on('$destroy', function() { 606 | dropContainer.detach(dropTarget); 607 | }); 608 | }, 609 | }; 610 | }, 611 | ]); 612 | 613 | function findDragActiveTarget(jqLite) { 614 | var body = jqLite.find('body'); 615 | 616 | return body.length ? body : jqLite; 617 | } 618 | 619 | 620 | /***/ }, 621 | /* 1 */ 622 | /***/ function(module, exports) { 623 | 624 | module.exports = __WEBPACK_EXTERNAL_MODULE_1__; 625 | 626 | /***/ } 627 | /******/ ]) 628 | }); 629 | ; -------------------------------------------------------------------------------- /dist/angular-drag-drop.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("angular")):"function"==typeof define&&define.amd?define(["angular"],e):"object"==typeof exports?exports.AngularDragDrop=e(require("angular")):t.AngularDragDrop=e(t.angular)}(this,function(t){return function(t){function e(a){if(r[a])return r[a].exports;var n=r[a]={exports:{},id:a,loaded:!1};return t[a].call(n.exports,n,n.exports,e),n.loaded=!0,n.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e,r){function a(t){var e=t.find("body");return e.length?e:t}var n=r(1);t.exports="filearts.dragDrop";var o=n.module(t.exports,[]),d='.drag-active .drop-container{position:relative}.drag-active .drop-container *{pointer-events:none}.drag-active .drop-container:before{position:absolute;top:0;right:0;bottom:0;left:0;z-index:9999;content:""}';o.factory("dragContext",[function(){function t(){return n.extend(r,{data:null,reset:t,start:e})}function e(t){return r.data=t,t}var r={};return t()}]),o.run(["$document","$rootElement","$timeout",function(t,e,r){function n(){i()}function o(){i()}function i(){r(function(){var t=a(e);t.removeClass("drag-active")})}function l(t,e){e||(e=document);var r=e.head||e.getElementsByTagName("head")[0];if(!r){r=e.createElement("head");var a=e.body||e.getElementsByTagName("body")[0];a?a.parentNode.insertBefore(r,a):e.documentElement.appendChild(r)}var n=e.createElement("style");return n.type="text/css",n.styleSheet?n.styleSheet.cssText=t:n.appendChild(e.createTextNode(t)),r.appendChild(n),n}t[0].addEventListener("dragend",n,!0),t[0].addEventListener("drop",o,!0),l(d,t[0])}]),o.directive("dragContainer",["$rootElement","$parse","$timeout","dragContext",function(t,e,r,n){return{restrict:"A",link:function(o,d,i){function l(e){if(r(function(){var e=a(t);e.addClass("drag-active")},0,!1),n.start(i.dragData?o.$eval(i.dragData):d),d.addClass("drag-container-active"),g){var l={$event:e,$dragData:n.data};o.$apply(function(){g(o,l)})}var s=e.originalEvent||e;s.dataTransfer&&(s.dataTransfer.items&&s.dataTransfer.items.length||s.dataTransfer.files&&s.dataTransfer.files.length||s.dataTransfer.setData("text",""))}function s(e){if(r(function(){var e=a(t);e.removeClass("drag-active")},0,!1),n.reset(),d.removeClass("drag-container-active"),c){var i={$event:e,$dragData:n.data};o.$apply(function(){c(o,i)})}n.lastTarget&&n.lastTarget.$attrs.$removeClass("drag-over")}var g=i.onDragStart?e(i.onDragStart):null,c=i.onDragEnd?e(i.onDragEnd):null;i.$addClass("drag-container"),o.$watch(i.dragContainer,function(t){i.$set("draggable","undefined"==typeof t||t)}),d.on("dragstart",l),d.on("dragend",s)}}}]),o.directive("dropContainer",["$document","$parse","$window","dragContext",function(t,e,r,a){function o(t){return t[0]||t}function d(e){e=o(e);var a=e.getBoundingClientRect();return{width:Math.round(n.isNumber(a.width)?a.width:e.offsetWidth),height:Math.round(n.isNumber(a.height)?a.height:e.offsetHeight),top:Math.round(a.top+(r.pageYOffset||t[0].documentElement.scrollTop)),left:Math.round(a.left+(r.pageXOffset||t[0].documentElement.scrollLeft))}}return{restrict:"A",require:"dropContainer",controller:"DropContainerController",controllerAs:"dropContainer",link:function(t,r,o,i){function l(e){a.lastTarget&&a.lastTarget!==r&&a.lastTarget.$attrs.$removeClass("drag-over"),a.lastTarget={$attrs:o,$element:r};var n={$event:e,$dragData:a.data};p(t,n)&&(e.preventDefault(),o.$addClass("drag-over"),u&&t.$apply(function(){u(t,n)}))}function s(e){var l={$event:e,$dragData:a.data};if(p(t,l)){e.preventDefault();var s=d(r);o.$addClass("drag-over");var g=Number.MAX_VALUE,c=s.width,u=s.height,v=e.pageX-s.left,h=e.pageY-s.top,$=i.lastTarget;n.forEach(i.targets,function(t,e){var r=c/2,a=u/2;e.indexOf("left")>=0&&(r=1*c/4),e.indexOf("top")>=0&&(a=1*u/4),e.indexOf("right")>=0&&(r=3*c/4),e.indexOf("bottom")>=0&&(a=3*u/4);var n=Math.pow(r-v,2)+Math.pow(a-h,2);n 2 | 3 | 4 | 5 | Angular Sliding Puzzle 6 | 7 | 8 | 9 |
10 |
20 |
24 |
37 |
38 |
39 | 40 | -------------------------------------------------------------------------------- /example/src/controllers/game.js: -------------------------------------------------------------------------------- 1 | var Angular = require('angular'); 2 | 3 | module.exports = 'dragular.controller.game'; 4 | 5 | Angular.module(module.exports, [ 6 | require('services/board'), 7 | ]) 8 | 9 | .config(['$locationProvider', function($locationProvider) { 10 | $locationProvider.html5Mode({ 11 | enabled: true, 12 | requireBase: false 13 | }); 14 | }]) 15 | 16 | .controller('GameController', ['$location', '$scope', 'board', function($location, $scope, board) { 17 | var game = this; 18 | var defaultImgUrl = 'http://s1.ibtimes.com/sites/www.ibtimes.com/files/styles/v2_article_large/public/2011/10/26/180060-a-pomeranian-dressed-as-zorro-the-spanish-masked-swordsman-in-the-movi.jpg'; 19 | var params = $location.search(); 20 | var clearWinListener; 21 | 22 | var watchForWin = function() { 23 | if (clearWinListener) clearWinListener(); 24 | 25 | clearWinListener = $scope.$watchCollection('game.board.pieces', function(pieces) { 26 | if (pieces.reduce(function(winning, pieceNum, pieceIdx) { 27 | return winning && pieceNum === pieceIdx; 28 | }, true)) { 29 | alert('You won in ' + game.moves + ' moves at difficulty ' + game.board.difficulty + '!'); 30 | 31 | init(); 32 | } 33 | }); 34 | }; 35 | 36 | var init = function() { 37 | game.moves = 0; 38 | 39 | game.board.init(params.img || defaultImgUrl, parseInt(params.grid, 10) || 4, parseInt(params.difficulty, 10) || 30) 40 | .then(watchForWin); 41 | }; 42 | 43 | game.board = board; 44 | 45 | game.move = function(idxA, idxB) { 46 | if (game.board.swap(idxA, idxB)) game.moves++; 47 | }; 48 | 49 | init(); 50 | }]) 51 | 52 | ; -------------------------------------------------------------------------------- /example/src/dragular.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | 13 | background-color: black; 14 | } 15 | 16 | .dr-board { 17 | display: flex; 18 | flex-direction: row; 19 | flex-wrap: wrap; 20 | min-height: 500px; 21 | min-height: 100vmin; 22 | 23 | width: 500px; 24 | width: 100vmin; 25 | height: 500px; 26 | height: 100vmin; 27 | 28 | background-color: whitesmoke; 29 | border: 6px inset #eee; 30 | } 31 | 32 | .dr-tile-img.swappable:hover { 33 | opacity: 0.8; 34 | cursor: move; 35 | } 36 | 37 | .dr-tile-empty, .dr-tile-img { 38 | width: 100%; 39 | height: 100%; 40 | } 41 | 42 | .dr-tile-img { 43 | border: 2px outset #eee; 44 | background-clip: border-box; 45 | background-size: contain; 46 | } -------------------------------------------------------------------------------- /example/src/dragular.js: -------------------------------------------------------------------------------- 1 | require("./dragular.css"); 2 | 3 | var Angular = require("angular"); 4 | 5 | Angular.module("dragular", [ 6 | require("angular-drag-drop"), 7 | 8 | require("controllers/game"), 9 | ]); -------------------------------------------------------------------------------- /example/src/services/board.js: -------------------------------------------------------------------------------- 1 | var Angular = require('angular'); 2 | 3 | module.exports = 'dragular.controller.board'; 4 | 5 | Angular.module(module.exports, []) 6 | 7 | .factory('board', ['$document', '$q', function($document, $q) { 8 | var board = {}; 9 | 10 | var loadImage = function(imgUrl) { 11 | var dfd = $q.defer(); 12 | var img = new Image(); 13 | 14 | var handleImageLoad = function(e) { 15 | dfd.resolve(img); 16 | }; 17 | 18 | var handleImageError = function(e) { 19 | dfd.reject(e); 20 | }; 21 | 22 | img.onload = handleImageLoad; 23 | img.onerror = handleImageError; 24 | img.crossOrigin = 'anonymous'; 25 | img.src = imgUrl; 26 | 27 | return dfd.promise; 28 | }; 29 | 30 | var createTiles = function(img) { 31 | var size = Math.min(img.width, img.height); 32 | var tileSize = Math.floor(size / board.grid); 33 | 34 | for (var x = 0; x < board.grid; x++) { 35 | for (var y = 0; y < board.grid; y++) { 36 | var canvas = document.createElement('canvas'); 37 | var ctx = canvas.getContext('2d'); 38 | 39 | canvas.width = tileSize; 40 | canvas.height = tileSize; 41 | 42 | ctx.drawImage(img, x * tileSize, y * tileSize, tileSize, tileSize, 0, 0, tileSize, tileSize); 43 | 44 | board.tiles[y * board.grid + x] = canvas.toDataURL(); 45 | } 46 | } 47 | 48 | return board.tiles; 49 | }; 50 | 51 | 52 | board.init = function(imgUrl, grid, difficulty) { 53 | board.imgUrl = imgUrl; 54 | board.grid = grid || 3; 55 | board.difficulty = difficulty || 16; 56 | board.pieces = Array.apply(0, Array(board.grid * board.grid)).map(function(v, k) { 57 | return k; 58 | }); 59 | board.tiles = Array.apply(0, Array(board.grid * board.grid)); 60 | 61 | return loadImage(board.imgUrl) 62 | .then(createTiles) 63 | .then(board.shuffle.bind(board)); 64 | }; 65 | 66 | board.shuffle = function() { 67 | var possibleMoves = []; 68 | var lastSwap = -1; 69 | 70 | for (var i = 0; i < board.difficulty; i++) { 71 | var emptyIdx = board.pieces.indexOf(0); 72 | var emptyPos = board.indexToPos(emptyIdx); 73 | 74 | possibleMoves.length = 0; 75 | 76 | if (emptyPos.x > 0) possibleMoves.push(emptyIdx - 1); 77 | if (emptyPos.y > 0) possibleMoves.push(emptyIdx - board.grid); 78 | if (emptyPos.x < board.grid - 1) possibleMoves.push(emptyIdx + 1); 79 | if (emptyPos.y < board.grid - 1) possibleMoves.push(emptyIdx + board.grid); 80 | 81 | possibleMoves = possibleMoves.filter(function(targetIdx) { 82 | return targetIdx !== lastSwap; 83 | }); 84 | 85 | var moveIdx = Math.floor(Math.random() * possibleMoves.length); 86 | var targetIdx = possibleMoves[moveIdx]; 87 | 88 | if (targetIdx >= board.pieces.length || emptyIdx >= board.pieces.length) debugger; 89 | 90 | board.pieces[emptyIdx] = board.pieces[targetIdx]; 91 | board.pieces[targetIdx] = 0; 92 | 93 | lastSwap = emptyIdx; 94 | } 95 | 96 | return board.pieces; 97 | }; 98 | 99 | board.posToIndex = function(posX, posY) { 100 | var pos = Angular.isObject(posX) ? posX : { 101 | x: posX, 102 | y: posY, 103 | }; 104 | 105 | return pos.y * board.grid + pos.x; 106 | }; 107 | 108 | board.indexToPos = function(idx) { 109 | var x = idx % board.grid; 110 | var y = Math.floor(idx / board.grid); 111 | 112 | return { 113 | x: x, 114 | y: y 115 | }; 116 | }; 117 | 118 | board.isAdjacent = function(idxA, idxB) { 119 | return (Math.floor(idxA / board.grid) === Math.floor(idxB / board.grid) && Math.abs(idxA - idxB) === 1) || Math.abs(idxA - idxB) === board.grid; 120 | }; 121 | 122 | board.swap = function(idxA, idxB) { 123 | if (!board.isAdjacent(idxA, idxB)) { 124 | return; 125 | } 126 | 127 | var tmp = board.pieces[idxA]; 128 | 129 | board.pieces[idxA] = board.pieces[idxB]; 130 | board.pieces[idxB] = tmp; 131 | 132 | return true; 133 | }; 134 | 135 | return board; 136 | 137 | }]) 138 | 139 | ; -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | var Path = require('path'); 2 | 3 | module.exports = { 4 | entry: { 5 | 'dragular': __dirname + '/src/dragular.js', 6 | }, 7 | output: { 8 | path: './build', 9 | pathInfo: false, 10 | publicPath: '/build/', 11 | filename: '[name].js', 12 | }, 13 | module: { 14 | loaders: [{ 15 | test: /\.html$/, 16 | loader: 'raw-loader' 17 | }, { 18 | test: /\.css$/, 19 | loader: 'style-loader!css-loader' 20 | }, { 21 | test: /\.less/, 22 | loader: 'style-loader!css-loader!less-loader' 23 | }, ], 24 | }, 25 | resolve: { 26 | modulesDirectories: ['node_modules', 'src'], 27 | root: __dirname, 28 | }, 29 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-drag-drop", 3 | "version": "3.2.0", 4 | "description": "Dependency-free drag and drop support in Angular.js", 5 | "main": "./src/angular-drag-drop.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack --config webpack.config.js", 9 | "build-example": "cd example && webpack", 10 | "start": "cd example && webpack-dev-server" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/ggoodman/angular-drag-drop.git" 15 | }, 16 | "keywords": [ 17 | "angular", 18 | "angularjs", 19 | "drag", 20 | "drop" 21 | ], 22 | "author": "Geoffrey Goodman", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/ggoodman/angular-drag-drop/issues" 26 | }, 27 | "homepage": "https://github.com/ggoodman/angular-drag-drop", 28 | "devDependencies": { 29 | "angular": "^1.5.8", 30 | "autoprefixer-loader": "^3.1.0", 31 | "css-loader": "^0.23.0", 32 | "eslint": "^4.17.0", 33 | "exports-loader": "^0.6.2", 34 | "less-loader": "^2.2.1", 35 | "style-loader": "^0.13.0", 36 | "webpack": "^1.12.9", 37 | "webpack-dev-server": "^1.14.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | ], 5 | parserOptions: { 6 | ecmaVersion: 5, 7 | }, 8 | env: { 9 | browser: true, 10 | commonjs: true, 11 | }, 12 | rules: { 13 | } 14 | }; -------------------------------------------------------------------------------- /src/angular-drag-drop.js: -------------------------------------------------------------------------------- 1 | var Angular = require('angular'); 2 | 3 | module.exports = 'filearts.dragDrop'; 4 | 5 | var mod = Angular.module(module.exports, []); 6 | var stylesheet = 7 | '.drag-active .drop-container{position:relative}.drag-active .drop-container *{pointer-events:none}.drag-active .drop-container:before{position:absolute;top:0;right:0;bottom:0;left:0;z-index:9999;content:""}'; 8 | 9 | mod.factory('dragContext', [ 10 | function() { 11 | var context = {}; 12 | 13 | return reset(); 14 | 15 | function reset() { 16 | return Angular.extend(context, { 17 | data: null, 18 | reset: reset, 19 | start: start, 20 | }); 21 | } 22 | 23 | function start(data) { 24 | context.data = data; 25 | 26 | return data; 27 | } 28 | }, 29 | ]); 30 | 31 | mod.run([ 32 | '$document', 33 | '$rootElement', 34 | '$timeout', 35 | function($document, $rootElement, $timeout) { 36 | $document[0].addEventListener('dragend', onDragEnd, true); 37 | $document[0].addEventListener('drop', onDrop, true); 38 | 39 | loadStyles(stylesheet, $document[0]); 40 | 41 | function onDragEnd() { 42 | clearDragActive(); 43 | } 44 | 45 | function onDrop() { 46 | clearDragActive(); 47 | } 48 | 49 | function clearDragActive() { 50 | $timeout(function() { 51 | var target = findDragActiveTarget($rootElement); 52 | 53 | target.removeClass('drag-active'); 54 | }); 55 | } 56 | 57 | /** 58 | * Load styles into the head element 59 | * 60 | * Source: https://github.com/webmodules/load-styles/blob/master/index.js 61 | */ 62 | function loadStyles(css, doc) { 63 | // default to the global `document` object 64 | if (!doc) doc = document; 65 | 66 | var head = doc.head || doc.getElementsByTagName('head')[0]; 67 | 68 | // no node? create one... 69 | if (!head) { 70 | head = doc.createElement('head'); 71 | var body = doc.body || doc.getElementsByTagName('body')[0]; 72 | if (body) { 73 | body.parentNode.insertBefore(head, body); 74 | } else { 75 | doc.documentElement.appendChild(head); 76 | } 77 | } 78 | 79 | var style = doc.createElement('style'); 80 | style.type = 'text/css'; 81 | if (style.styleSheet) { 82 | // IE 83 | style.styleSheet.cssText = css; 84 | } else { 85 | // the world 86 | style.appendChild(doc.createTextNode(css)); 87 | } 88 | head.appendChild(style); 89 | 90 | return style; 91 | } 92 | }, 93 | ]); 94 | 95 | mod.directive('dragContainer', [ 96 | '$rootElement', 97 | '$parse', 98 | '$timeout', 99 | 'dragContext', 100 | function($rootElement, $parse, $timeout, dragContext) { 101 | return { 102 | restrict: 'A', 103 | link: function($scope, $element, $attrs) { 104 | var onDragStart = $attrs.onDragStart 105 | ? $parse($attrs.onDragStart) 106 | : null; 107 | var onDragEnd = $attrs.onDragEnd 108 | ? $parse($attrs.onDragEnd) 109 | : null; 110 | 111 | $attrs.$addClass('drag-container'); 112 | 113 | $scope.$watch($attrs.dragContainer, function(draggable) { 114 | $attrs.$set( 115 | 'draggable', 116 | typeof draggable === 'undefined' || draggable 117 | ); 118 | }); 119 | 120 | $element.on('dragstart', handleDragStart); 121 | $element.on('dragend', handleDragEnd); 122 | 123 | function handleDragStart(e) { 124 | $timeout( 125 | function() { 126 | var target = findDragActiveTarget($rootElement); 127 | 128 | target.addClass('drag-active'); 129 | }, 130 | 0, 131 | false 132 | ); 133 | 134 | dragContext.start( 135 | $attrs.dragData 136 | ? $scope.$eval($attrs.dragData) 137 | : $element 138 | ); 139 | $element.addClass('drag-container-active'); 140 | 141 | if (onDragStart) { 142 | var locals = { 143 | $event: e, 144 | $dragData: dragContext.data, 145 | }; 146 | 147 | $scope.$apply(function() { 148 | onDragStart($scope, locals); 149 | }); 150 | } 151 | 152 | var targetEvent = e.originalEvent || e; 153 | 154 | if (targetEvent.dataTransfer) { 155 | if ( 156 | (!targetEvent.dataTransfer.items || 157 | !targetEvent.dataTransfer.items.length) && 158 | (!targetEvent.dataTransfer.files || 159 | !targetEvent.dataTransfer.files.length) 160 | ) { 161 | targetEvent.dataTransfer.setData('text', ''); 162 | } 163 | } 164 | } 165 | 166 | function handleDragEnd(e) { 167 | $timeout( 168 | function() { 169 | var target = findDragActiveTarget($rootElement); 170 | 171 | target.removeClass('drag-active'); 172 | }, 173 | 0, 174 | false 175 | ); 176 | 177 | dragContext.reset(); 178 | $element.removeClass('drag-container-active'); 179 | 180 | if (onDragEnd) { 181 | var locals = { 182 | $event: e, 183 | $dragData: dragContext.data, 184 | }; 185 | 186 | $scope.$apply(function() { 187 | onDragEnd($scope, locals); 188 | }); 189 | } 190 | 191 | if (dragContext.lastTarget) { 192 | dragContext.lastTarget.$attrs.$removeClass('drag-over'); 193 | } 194 | } 195 | }, 196 | }; 197 | }, 198 | ]); 199 | 200 | mod.directive('dropContainer', [ 201 | '$document', 202 | '$parse', 203 | '$window', 204 | 'dragContext', 205 | function($document, $parse, $window, dragContext) { 206 | return { 207 | restrict: 'A', 208 | require: 'dropContainer', 209 | controller: 'DropContainerController', 210 | controllerAs: 'dropContainer', 211 | link: function($scope, $element, $attrs, dropContainer) { 212 | var acceptsFn = $attrs.dropAccepts 213 | ? $parse($attrs.dropAccepts) 214 | : function($scope, locals) { 215 | return typeof locals.$dragData !== 'undefined'; 216 | }; 217 | var onDragEnter = $attrs.onDragEnter 218 | ? $parse($attrs.onDragEnter) 219 | : null; 220 | var onDragOver = $attrs.onDragOver 221 | ? $parse($attrs.onDragOver) 222 | : null; 223 | var onDragLeave = $attrs.onDragLeave 224 | ? $parse($attrs.onDragLeave) 225 | : null; 226 | var onDrop = $attrs.onDrop ? $parse($attrs.onDrop) : null; 227 | 228 | $attrs.$addClass('drop-container'); 229 | 230 | $element.on('dragover', handleDragOver); 231 | $element.on('dragenter', handleDragEnter); 232 | $element.on('dragleave', handleDragLeave); 233 | $element.on('drop', handleDrop); 234 | 235 | function handleDragEnter(e) { 236 | if ( 237 | dragContext.lastTarget && 238 | dragContext.lastTarget !== $element 239 | ) { 240 | dragContext.lastTarget.$attrs.$removeClass('drag-over'); 241 | } 242 | 243 | dragContext.lastTarget = { 244 | $attrs: $attrs, 245 | $element: $element, 246 | }; 247 | 248 | var locals = { 249 | $event: e, 250 | $dragData: dragContext.data, 251 | }; 252 | 253 | if (acceptsFn($scope, locals)) { 254 | e.preventDefault(); 255 | 256 | $attrs.$addClass('drag-over'); 257 | 258 | if (onDragEnter) { 259 | $scope.$apply(function() { 260 | onDragEnter($scope, locals); 261 | }); 262 | } 263 | } 264 | } 265 | 266 | function handleDragOver(e) { 267 | var locals = { 268 | $event: e, 269 | $dragData: dragContext.data, 270 | }; 271 | 272 | if (acceptsFn($scope, locals)) { 273 | e.preventDefault(); 274 | 275 | var pos = offset($element); 276 | 277 | $attrs.$addClass('drag-over'); 278 | 279 | var minDistanceSq = Number.MAX_VALUE; 280 | var width = pos.width; 281 | var height = pos.height; 282 | var x = e.pageX - pos.left; 283 | var y = e.pageY - pos.top; 284 | var closestTarget = dropContainer.lastTarget; 285 | 286 | Angular.forEach(dropContainer.targets, function( 287 | dropTarget, 288 | anchor 289 | ) { 290 | var anchorX = width / 2; 291 | var anchorY = height / 2; 292 | 293 | if (anchor.indexOf('left') >= 0) 294 | anchorX = width * 1 / 4; 295 | if (anchor.indexOf('top') >= 0) 296 | anchorY = height * 1 / 4; 297 | if (anchor.indexOf('right') >= 0) 298 | anchorX = width * 3 / 4; 299 | if (anchor.indexOf('bottom') >= 0) 300 | anchorY = height * 3 / 4; 301 | 302 | var distanceSq = 303 | Math.pow(anchorX - x, 2) + 304 | Math.pow(anchorY - y, 2); 305 | 306 | if (distanceSq < minDistanceSq) { 307 | closestTarget = dropTarget; 308 | minDistanceSq = distanceSq; 309 | } 310 | }); 311 | 312 | $scope.$apply(function() { 313 | if (onDragOver) { 314 | onDragOver($scope, locals); 315 | } 316 | 317 | if (!closestTarget) return; 318 | 319 | if (closestTarget !== dropContainer.lastTarget) { 320 | if (dropContainer.lastTarget) { 321 | $attrs.$removeClass( 322 | 'drop-container-' + 323 | dropContainer.lastTarget.anchor 324 | ); 325 | } 326 | 327 | $attrs.$addClass( 328 | 'drop-container-' + closestTarget.anchor 329 | ); 330 | 331 | if (dropContainer.lastTarget) { 332 | dropContainer.lastTarget.handleDragLeave( 333 | e, 334 | locals 335 | ); 336 | } 337 | 338 | closestTarget.handleDragEnter(e, locals); 339 | 340 | dropContainer.lastTarget = closestTarget; 341 | } 342 | 343 | closestTarget.handleDragOver(e); 344 | }); 345 | } 346 | } 347 | 348 | function handleDragLeave(e) { 349 | $attrs.$removeClass('drag-over'); 350 | 351 | var locals = { 352 | $event: e, 353 | $dragData: dragContext.data, 354 | }; 355 | 356 | $scope.$apply(function() { 357 | if (onDragLeave) { 358 | onDragLeave($scope, locals); 359 | } 360 | 361 | if (dropContainer.lastTarget) { 362 | dropContainer.lastTarget.handleDragLeave(e, locals); 363 | } 364 | 365 | if (dropContainer.lastTarget) { 366 | $attrs.$removeClass( 367 | 'drop-container-' + 368 | dropContainer.lastTarget.anchor 369 | ); 370 | 371 | dropContainer.lastTarget = null; 372 | } 373 | }); 374 | } 375 | 376 | function handleDrop(e) { 377 | if (dragContext.lastTarget) { 378 | dragContext.lastTarget.$attrs.$removeClass('drag-over'); 379 | } 380 | 381 | var locals = { 382 | $event: e, 383 | $dragData: dragContext.data, 384 | }; 385 | 386 | if (acceptsFn($scope, locals)) { 387 | e.preventDefault(); 388 | dragContext.reset(); 389 | 390 | $scope.$apply(function() { 391 | if (onDrop) { 392 | onDrop($scope, locals); 393 | } 394 | 395 | if (dropContainer.lastTarget) { 396 | dropContainer.lastTarget.handleDrop(e, locals); 397 | } 398 | }); 399 | } 400 | 401 | if (dropContainer.lastTarget) { 402 | $attrs.$removeClass( 403 | 'drop-container-' + dropContainer.lastTarget.anchor 404 | ); 405 | } 406 | 407 | dropContainer.lastTarget = null; 408 | } 409 | }, 410 | }; 411 | 412 | // Source: https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js 413 | function getRawNode(elem) { 414 | return elem[0] || elem; 415 | } 416 | 417 | // Source: https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js 418 | function offset(elem) { 419 | elem = getRawNode(elem); 420 | 421 | var elemBCR = elem.getBoundingClientRect(); 422 | return { 423 | width: Math.round( 424 | Angular.isNumber(elemBCR.width) 425 | ? elemBCR.width 426 | : elem.offsetWidth 427 | ), 428 | height: Math.round( 429 | Angular.isNumber(elemBCR.height) 430 | ? elemBCR.height 431 | : elem.offsetHeight 432 | ), 433 | top: Math.round( 434 | elemBCR.top + 435 | ($window.pageYOffset || 436 | $document[0].documentElement.scrollTop) 437 | ), 438 | left: Math.round( 439 | elemBCR.left + 440 | ($window.pageXOffset || 441 | $document[0].documentElement.scrollLeft) 442 | ), 443 | }; 444 | } 445 | }, 446 | ]); 447 | 448 | mod.controller('DropContainerController', [ 449 | function() { 450 | var dropContainer = this; 451 | var validAnchors = 'center top top-right right bottom-right bottom bottom-left left top-left'.split( 452 | ' ' 453 | ); 454 | 455 | dropContainer.targets = {}; 456 | dropContainer.lastTarget = null; 457 | 458 | dropContainer.attach = function(dropTarget) { 459 | var anchor = dropTarget.anchor; 460 | 461 | if (validAnchors.indexOf(anchor) < 0) { 462 | throw new Error('Invalid drop target anchor `' + anchor + '`.'); 463 | } 464 | 465 | dropContainer.targets[anchor] = dropTarget; 466 | 467 | return dropTarget; 468 | }; 469 | 470 | dropContainer.detach = function(dropTarget) { 471 | var anchor = dropTarget.anchor; 472 | 473 | if (validAnchors.indexOf(anchor) < 0) { 474 | throw new Error('Invalid drop target anchor `' + anchor + '`.'); 475 | } 476 | 477 | if (!dropContainer.targets[anchor] === dropTarget) { 478 | throw new Error( 479 | 'The indicated drop target is not attached at ' + 480 | 'the anchor `' + 481 | anchor + 482 | '`.' 483 | ); 484 | } 485 | 486 | delete dropContainer.targets[anchor]; 487 | 488 | return dropTarget; 489 | }; 490 | }, 491 | ]); 492 | 493 | mod.directive('dropTarget', [ 494 | '$parse', 495 | function($parse) { 496 | return { 497 | restrict: 'A', 498 | require: ['^dropContainer', 'dropTarget'], 499 | scope: true, 500 | bindToController: { 501 | anchor: '@dropTarget', 502 | }, 503 | controller: Angular.noop, 504 | controllerAs: 'dropTarget', 505 | link: function($scope, $element, $attrs, ctls) { 506 | var dropContainer = ctls[0]; 507 | var dropTarget = ctls[1]; 508 | 509 | $attrs.$addClass('drop-target'); 510 | 511 | dropTarget.$attrs = $attrs; 512 | dropTarget.$scope = $scope; 513 | 514 | $attrs.$addClass( 515 | 'drop-target drop-target-' + dropTarget.anchor 516 | ); 517 | 518 | dropContainer.attach(dropTarget); 519 | 520 | var onDragEnter = dropTarget.$attrs.onDragEnter 521 | ? $parse(dropTarget.$attrs.onDragEnter) 522 | : Angular.noop; 523 | var onDragLeave = dropTarget.$attrs.onDragLeave 524 | ? $parse(dropTarget.$attrs.onDragLeave) 525 | : Angular.noop; 526 | var onDragOver = dropTarget.$attrs.onDragOver 527 | ? $parse(dropTarget.$attrs.onDragOver) 528 | : Angular.noop; 529 | var onDrop = dropTarget.$attrs.onDrop 530 | ? $parse(dropTarget.$attrs.onDrop) 531 | : Angular.noop; 532 | 533 | dropTarget.handleDragEnter = function(e, locals) { 534 | onDragEnter(dropTarget.$scope, locals); 535 | }; 536 | 537 | dropTarget.handleDragLeave = function(e, locals) { 538 | onDragLeave(dropTarget.$scope, locals); 539 | }; 540 | 541 | dropTarget.handleDragOver = function(e, locals) { 542 | onDragOver(dropTarget.$scope, locals); 543 | }; 544 | 545 | dropTarget.handleDrop = function(e, locals) { 546 | onDrop(dropTarget.$scope, locals); 547 | }; 548 | 549 | $scope.$on('$destroy', function() { 550 | dropContainer.detach(dropTarget); 551 | }); 552 | }, 553 | }; 554 | }, 555 | ]); 556 | 557 | function findDragActiveTarget(jqLite) { 558 | var body = jqLite.find('body'); 559 | 560 | return body.length ? body : jqLite; 561 | } 562 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | 5 | const devConfig = { 6 | cache: true, 7 | entry: { 8 | 'angular-drag-drop': __dirname + "/src/angular-drag-drop.js" 9 | }, 10 | output: { 11 | libraryTarget: "umd", 12 | library: "AngularDragDrop", 13 | path: "./dist", 14 | filename: "angular-drag-drop.js" 15 | }, 16 | externals: { 17 | 'angular': 'angular' 18 | } 19 | }; 20 | 21 | const prodConfig = { 22 | cache: true, 23 | entry: { 24 | 'angular-drag-drop': __dirname + "/src/angular-drag-drop.js" 25 | }, 26 | output: { 27 | libraryTarget: "umd", 28 | library: "AngularDragDrop", 29 | path: "./dist", 30 | filename: "angular-drag-drop.min.js" 31 | }, 32 | externals: { 33 | 'angular': 'angular' 34 | }, 35 | plugins: [ 36 | new webpack.optimize.UglifyJsPlugin() 37 | ] 38 | }; 39 | 40 | module.exports = [devConfig, prodConfig]; 41 | --------------------------------------------------------------------------------