├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── config ├── helpers.js ├── karma.conf.js ├── spec-bundle.js ├── testing-utils.ts └── webpack.test.js ├── demo ├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── README.md ├── e2e │ ├── app.e2e-spec.ts │ ├── app.po.ts │ └── tsconfig.e2e.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── app.router.ts │ │ ├── examples │ │ │ ├── demo-dnd.router.ts │ │ │ ├── dnd │ │ │ │ ├── custom-data │ │ │ │ │ └── custom-data.component.ts │ │ │ │ ├── custom-function │ │ │ │ │ └── custom-function.component.ts │ │ │ │ ├── shopping-basket │ │ │ │ │ └── shopping-basket.component.ts │ │ │ │ ├── simple │ │ │ │ │ ├── index.ts │ │ │ │ │ └── simple.component.ts │ │ │ │ └── zone │ │ │ │ │ └── zone.component.ts │ │ │ ├── index.ts │ │ │ └── sortable │ │ │ │ ├── embedded │ │ │ │ └── embedded.component.ts │ │ │ │ ├── multi │ │ │ │ └── multi.component.ts │ │ │ │ ├── recycle-multi │ │ │ │ └── recycle-multi.component.ts │ │ │ │ ├── simple-sortable-copy │ │ │ │ └── simple-sortable-copy.component.ts │ │ │ │ └── simple │ │ │ │ └── simple.component.ts │ │ └── shared │ │ │ ├── index.ts │ │ │ └── side-nav │ │ │ ├── side-nav.component.html │ │ │ └── side-nav.component.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ ├── test.ts │ ├── tsconfig.app.json │ └── tsconfig.spec.json ├── tsconfig.json └── tslint.json ├── karma.conf.js ├── ng-package.json ├── package-lock.json ├── package.json ├── public_api.ts ├── src ├── abstract.component.ts ├── dnd.config.ts ├── dnd.module.ts ├── dnd.service.ts ├── dnd.utils.ts ├── draggable.component.ts ├── droppable.component.ts └── sortable.component.ts ├── style.css ├── tests ├── dnd.component.factory.ts ├── dnd.draggable.handle.spec.ts ├── dnd.sortable.handle.spec.ts ├── dnd.sortable.spec.ts ├── dnd.with.draggable.data.spec.ts └── dnd.without.draggable.data.spec.ts ├── tsconfig.json └── tslint.json /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Submitting Pull Requests 2 | If you're changing the structure of the repository please create an issue first 3 | 4 | ## Submitting bug reports 5 | 6 | Make sure you are on latest changes and that you ran this command `npm install` after updating your local repository. If you can, please provide more infomation about your environment such as browser, operating system, node version, and npm version -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * **I'm submitting a ...** 2 | [ ] bug report 3 | [ ] feature request 4 | [ ] question about the decisions made in the repository 5 | 6 | * **Do you want to request a *feature* or report a *bug*?** 7 | 8 | 9 | 10 | * **What is the current behavior?** 11 | 12 | 13 | 14 | * **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem** via https://plnkr.co or similar. 15 | 16 | 17 | 18 | * **What is the expected behavior?** 19 | 20 | 21 | 22 | * **What is the motivation / use case for changing the behavior?** 23 | 24 | 25 | 26 | * **Please tell us about your environment:** 27 | 28 | - Angular version: 2.X.X 29 | - Browser: [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ] 30 | 31 | 32 | 33 | * **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc) 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 2 | 3 | 4 | 5 | * **What is the current behavior?** (You can also link to an open issue here) 6 | 7 | 8 | 9 | * **What is the new behavior (if this is a feature change)?** 10 | 11 | 12 | 13 | * **Other information**: 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Misc 3 | ################# 4 | **/.DS_Store 5 | nbproject 6 | manifest.mf 7 | build.xml 8 | node_modules/* 9 | npm-debug.log 10 | *.js 11 | !config/* 12 | !karma.conf.js 13 | !webpack.config.js 14 | *.map 15 | *.d.ts 16 | !make.js 17 | coverage 18 | *.metadata.json 19 | bundles 20 | .vscode 21 | dist 22 | 23 | ################# 24 | ## JetBrains 25 | ################# 26 | .idea 27 | .project 28 | .settings 29 | 30 | ############ 31 | ## Windows 32 | ############ 33 | 34 | # Windows image file caches 35 | Thumbs.db 36 | 37 | # Folder config file 38 | Desktop.ini 39 | 40 | ############ 41 | ## Mac 42 | ############ 43 | 44 | # Mac crap 45 | .DS_Store 46 | 47 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Misc 3 | ################# 4 | **/.DS_Store 5 | nbproject 6 | manifest.mf 7 | build.xml 8 | node_modules/* 9 | npm-debug.log 10 | *.ts 11 | !*.d.ts 12 | tests 13 | .github 14 | coverage 15 | !*.metadata.json 16 | !bundles/*.js 17 | 18 | ################# 19 | ## JetBrains 20 | ################# 21 | .idea 22 | .project 23 | .settings 24 | 25 | ############ 26 | ## Windows 27 | ############ 28 | 29 | # Windows image file caches 30 | Thumbs.db 31 | 32 | # Folder config file 33 | Desktop.ini 34 | 35 | ############ 36 | ## Mac 37 | ############ 38 | 39 | # Mac crap 40 | .DS_Store 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | cache: 4 | directories: 5 | - $HOME/.npm 6 | - $HOME/.yarn-cache 7 | - node_modules 8 | 9 | sudo: false 10 | 11 | notifications: 12 | email: false 13 | 14 | node_js: 15 | - '8' 16 | 17 | branches: 18 | except: 19 | - "/^v\\d+\\.\\d+\\.\\d+$/" 20 | 21 | before_install: 22 | - export CHROME_BIN=chromium-browser 23 | - npm i -g yarn 24 | 25 | before_script: 26 | - npm prune 27 | 28 | install: 29 | - yarn 30 | 31 | before_script: 32 | - export DISPLAY=:99.0 33 | - sh -e /etc/init.d/xvfb start -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sergey Akopkokhyants 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular 2 Drag-and-Drop [![npm version](https://badge.fury.io/js/ng2-dnd.svg)](https://badge.fury.io/js/ng2-dnd) [![npm monthly downloads](https://img.shields.io/npm/dm/ng2-dnd.svg?style=flat-square)](https://www.npmjs.com/package/ng2-dnd) 2 | Angular 2 Drag-and-Drop without dependencies. 3 | 4 | Follow me [![twitter](https://img.shields.io/twitter/follow/akopkokhyants.svg?style=social&label=%20akopkokhyants)](https://twitter.com/akopkokhyants) to be notified about new releases. 5 | 6 | [![Build Status](https://travis-ci.org/akserg/ng2-dnd.svg?branch=master)](https://travis-ci.org/akserg/ng2-dnd) 7 | [![Dependency Status](https://david-dm.org/akserg/ng2-dnd.svg)](https://david-dm.org/akserg/ng2-dnd) 8 | [![devDependency Status](https://david-dm.org/akserg/ng2-dnd/dev-status.svg)](https://david-dm.org/akserg/ng2-dnd#info=devDependencies) 9 | [![Known Vulnerabilities](https://snyk.io/test/github/akserg/ng2-dnd/badge.svg)](https://snyk.io/test/github/akserg/ng2-dnd) 10 | 11 | _Some of these APIs and Components are not final and are subject to change!_ 12 | 13 | ## Transpilation to Angular Package Format 14 | The library uses [ng-packagr](https://github.com/dherges/ng-packagr) to transpile into the Angular Package Format: 15 | - Bundles library in `FESM2015`, `FESM5`, and `UMD` formats 16 | - The npm package can be consumed by `Angular CLI`, `Webpack`, or `SystemJS` 17 | - Creates type definitions (`.d.ts`) 18 | - Generates Ahead-of-Time metadata (`.metadata.json`) 19 | - Auto-discovers and bundles secondary entry points such as `@my/foo`, `@my/foo/testing`, `@my/foo/bar` 20 | 21 | ## Installation 22 | ```bash 23 | npm install ng2-dnd --save 24 | ``` 25 | 26 | ## Demo 27 | - Webpack demo available [here](https://angular-dxqjhj.stackblitz.io) 28 | - SystemJS demo available [here](http://embed.plnkr.co/JbG8Si) 29 | 30 | ## Usage 31 | If you use SystemJS to load your files, you might have to update your config: 32 | 33 | ```js 34 | System.config({ 35 | map: { 36 | 'ng2-dnd': 'node_modules/ng2-dnd/bundles/ng2-dnd.umd.js' 37 | } 38 | }); 39 | ``` 40 | 41 | #### 1. Add the default styles 42 | - Import the `style.css` into your web page from `node_modules/ng2-dnd/bundles/style.css` 43 | 44 | #### 2. Import the `DndModule` 45 | Import `DndModule.forRoot()` in the NgModule of your application. 46 | The `forRoot` method is a convention for modules that provide a singleton service. 47 | 48 | ```ts 49 | import {BrowserModule} from "@angular/platform-browser"; 50 | import {NgModule} from '@angular/core'; 51 | import {DndModule} from 'ng2-dnd'; 52 | 53 | @NgModule({ 54 | imports: [ 55 | BrowserModule, 56 | DndModule.forRoot() 57 | ], 58 | bootstrap: [AppComponent] 59 | }) 60 | export class AppModule { 61 | } 62 | ``` 63 | 64 | If you have multiple NgModules and you use one as a shared NgModule (that you import in all of your other NgModules), 65 | don't forget that you can use it to export the `DndModule` that you imported in order to avoid having to import it multiple times. 66 | 67 | ```ts 68 | @NgModule({ 69 | imports: [ 70 | BrowserModule, 71 | DndModule 72 | ], 73 | exports: [BrowserModule, DndModule], 74 | }) 75 | export class SharedModule { 76 | } 77 | ``` 78 | 79 | #### 3. Use Drag-and-Drop operations with no code 80 | 81 | ```js 82 | import {Component} from '@angular/core'; 83 | 84 | @Component({ 85 | selector: 'simple-dnd', 86 | template: ` 87 |

Simple Drag-and-Drop

88 |
89 |
90 |
91 |
Available to drag
92 |
93 |
94 |
95 |
Drag Me
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
Place to drop
104 |
105 |
Item was dropped here
106 |
107 |
108 |
109 |
` 110 | }) 111 | export class SimpleDndComponent { 112 | simpleDrop: any = null; 113 | } 114 | ``` 115 | 116 | #### 4. Add handle to restrict draggable zone of component 117 | 118 | ```js 119 | import {Component} from '@angular/core'; 120 | 121 | @Component({ 122 | selector: 'simple-dnd-handle', 123 | template: ` 124 |

Simple Drag-and-Drop with handle

125 |
126 |
127 |
128 |
Available to drag
129 |
130 |
131 |
132 |
133 | =  134 | Drag Handle 135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
Place to drop
144 |
145 |
Item was dropped here
146 |
147 |
148 |
149 |
` 150 | }) 151 | export class SimpleDndHandleComponent { 152 | simpleDrop: any = null; 153 | } 154 | ``` 155 | 156 | #### 5. Restriction Drag-and-Drop operations with drop zones 157 | You can use property *dropZones* (actually an array) to specify in which place you would like to drop the draggable element: 158 | 159 | ```js 160 | import {Component} from '@angular/core'; 161 | 162 | @Component({ 163 | selector: 'zone-dnd', 164 | template: ` 165 |

Restricted Drag-and-Drop with zones

166 |
167 |
168 |
169 |
Available to drag
170 |
171 |
172 |
173 |
Drag Me
174 |
Zone 1 only
175 |
176 |
177 |
178 |
179 | 180 |
181 |
Available to drag
182 |
183 |
184 |
185 |
Drag Me
186 |
Zone 1 & 2
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
Zone 1
195 |
196 |
Item was dropped here
197 |
198 |
199 |
200 |
201 |
202 |
Zone 2
203 |
204 |
Item was dropped here
205 |
206 |
207 |
208 |
` 209 | }) 210 | export class ZoneDndComponent { 211 | restrictedDrop1: any = null; 212 | restrictedDrop2: any = null; 213 | } 214 | ``` 215 | 216 | #### 6. Transfer custom data via Drag-and-Drop 217 | You can transfer data from draggable to droppable component via *dragData* property of Draggable component: 218 | 219 | ```js 220 | import {Component} from '@angular/core'; 221 | 222 | @Component({ 223 | selector: 'custom-data-dnd', 224 | template: ` 225 |

Transfer custom data in Drag-and-Drop

226 |
227 |
228 |
229 |
Available to drag
230 |
231 |
232 |
233 |
Drag Me
234 |
{{transferData | json}}
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
Place to drop (Items:{{receivedData.length}})
243 |
244 |
{{data | json}}
245 |
246 |
247 |
248 |
` 249 | }) 250 | export class CustomDataDndComponent { 251 | transferData: Object = {id: 1, msg: 'Hello'}; 252 | receivedData: Array = []; 253 | 254 | transferDataSuccess($event: any) { 255 | this.receivedData.push($event); 256 | } 257 | } 258 | ``` 259 | 260 | #### 7. Use a custom function to determine where dropping is allowed 261 | For use-cases when a static set of `dropZone`s is not possible, a custom function can be used to dynamically determine whether an item can be dropped or not. To achieve that, set the `allowDrop` property to this boolean function. 262 | 263 | In the following example, we have two containers that only accept numbers that are multiples of a user-input base integer. `dropZone`s are not helpful here because they are static, whereas the user input is dynamic. 264 | 265 | ```js 266 | import { Component } from '@angular/core'; 267 | 268 | @Component({ 269 | selector: 'custom-function-dnd', 270 | template: ` 271 |

Use a custom function to determine where dropping is allowed

272 |
273 |
274 |
275 |
Available to drag
276 |
277 |
278 |
dragData = 6
279 |
280 |
281 |
dragData = 10
282 |
283 |
284 |
dragData = 30
285 |
286 |
287 |
288 |
289 |
290 |
allowDropFunction(baseInteger: any): any {{ '{' }}
291 |   return (dragData: any) => dragData % baseInteger === 0;
292 | {{ '}' }}
293 |
294 |
295 |
296 |
297 | Multiples of 298 | 299 | only 300 |
301 |
302 |
dragData = {{item}}
303 |
304 |
305 |
306 |
307 |
308 |
309 | Multiples of 310 | 311 | only 312 |
313 |
314 |
dragData = {{item}}
315 |
316 |
317 |
318 |
319 |
320 |
321 | ` 322 | }) 323 | export class CustomFunctionDndComponent { 324 | box1Integer: number = 3; 325 | box2Integer: number = 10; 326 | 327 | box1Items: string[] = []; 328 | box2Items: string[] = []; 329 | 330 | allowDropFunction(baseInteger: number): any { 331 | return (dragData: any) => dragData % baseInteger === 0; 332 | } 333 | 334 | addTobox1Items($event: any) { 335 | this.box1Items.push($event.dragData); 336 | } 337 | 338 | addTobox2Items($event: any) { 339 | this.box2Items.push($event.dragData); 340 | } 341 | } 342 | ``` 343 | 344 | #### 8. Shopping basket with Drag-and-Drop 345 | Here is an example of shopping backet with products adding via drag and drop operation: 346 | 347 | ```js 348 | import { Component } from '@angular/core'; 349 | 350 | @Component({ 351 | selector: 'shoping-basket-dnd', 352 | template: ` 353 |

Drag-and-Drop - Shopping basket

354 |
355 | 356 |
357 |
358 |
Available products
359 |
360 |
362 |
363 |
{{product.name}} - \${{product.cost}}
(available: {{product.quantity}})
364 |
{{product.name}}
(NOT available)
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
Shopping Basket
(to pay: \${{totalCost()}})
373 |
374 |
375 |
376 | {{product.name}}
(ordered: {{product.quantity}}
cost: \${{product.cost * product.quantity}}) 377 |
378 |
379 |
380 |
381 |
382 |
` 383 | }) 384 | export class ShoppingBasketDndComponent { 385 | availableProducts: Array = []; 386 | shoppingBasket: Array = []; 387 | 388 | constructor() { 389 | this.availableProducts.push(new Product('Blue Shoes', 3, 35)); 390 | this.availableProducts.push(new Product('Good Jacket', 1, 90)); 391 | this.availableProducts.push(new Product('Red Shirt', 5, 12)); 392 | this.availableProducts.push(new Product('Blue Jeans', 4, 60)); 393 | } 394 | 395 | orderedProduct($event: any) { 396 | let orderedProduct: Product = $event.dragData; 397 | orderedProduct.quantity--; 398 | } 399 | 400 | addToBasket($event: any) { 401 | let newProduct: Product = $event.dragData; 402 | for (let indx in this.shoppingBasket) { 403 | let product: Product = this.shoppingBasket[indx]; 404 | if (product.name === newProduct.name) { 405 | product.quantity++; 406 | return; 407 | } 408 | } 409 | this.shoppingBasket.push(new Product(newProduct.name, 1, newProduct.cost)); 410 | this.shoppingBasket.sort((a: Product, b: Product) => { 411 | return a.name.localeCompare(b.name); 412 | }); 413 | } 414 | 415 | totalCost(): number { 416 | let cost: number = 0; 417 | for (let indx in this.shoppingBasket) { 418 | let product: Product = this.shoppingBasket[indx]; 419 | cost += (product.cost * product.quantity); 420 | } 421 | return cost; 422 | } 423 | } 424 | 425 | class Product { 426 | constructor(public name: string, public quantity: number, public cost: number) {} 427 | } 428 | ``` 429 | 430 | #### 9. Simple sortable with Drag-and-Drop 431 | Here is an example of simple sortable of favorite drinks moving in container via drag and drop operation: 432 | 433 | ```js 434 | import {Component} from '@angular/core'; 435 | 436 | @Component({ 437 | selector: 'simple-sortable', 438 | template: ` 439 |

Simple sortable

440 |
441 |
442 |
443 |
444 | Favorite drinks 445 |
446 |
447 |
    448 |
  • {{item}}
  • 449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 | My prefences:
457 | {{i + 1}}) {{item}}
458 |
459 |
460 |
461 |
` 462 | }) 463 | export class SimpleSortableComponent { 464 | listOne: Array = ['Coffee', 'Orange Juice', 'Red Wine', 'Unhealty drink!', 'Water']; 465 | } 466 | ``` 467 | 468 | 469 | #### 10. Simple sortable with Drag-and-Drop handle 470 | Add handle to restict grip zone of sortable component. 471 | 472 | ```js 473 | import {Component} from '@angular/core'; 474 | 475 | @Component({ 476 | selector: 'simple-sortable-handle', 477 | template: ` 478 |

Simple sortable handle

479 |
480 |
481 |
482 |
483 | Favorite drinks 484 |
485 |
486 |
    487 |
  • 488 | =  489 | {{item}} 490 |
  • 491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 | My prefences:
499 | {{i + 1}}) {{item}}
500 |
501 |
502 |
503 |
` 504 | }) 505 | export class SimpleSortableHandleComponent { 506 | listOne: Array = ['Coffee', 'Orange Juice', 'Red Wine', 'Unhealty drink!', 'Water']; 507 | } 508 | ``` 509 | 510 | #### 11. Simple sortable With Drop into recycle bin 511 | Here is an example of multi list sortable of boxers moving in container and between containers via drag and drop operation: 512 | 513 | ```js 514 | import {Component} from '@angular/core'; 515 | 516 | @Component({ 517 | selector: 'recycle-multi-sortable', 518 | template: ` 519 |

Simple sortable With Drop into recycle bin

520 |
521 |
522 |
523 |
524 | Favorite drinks 525 |
526 |
527 |
    528 |
  • {{item}}
  • 530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 | Recycle bin: Drag into me to delete it
538 |
539 |
540 |
541 | Recycled: {{listRecycled.toString()}} 542 |
543 |
544 |
` 545 | }) 546 | export class RecycleMultiSortableComponent { 547 | listOne: Array = ['Coffee', 'Orange Juice', 'Red Wine', 'Unhealty drink!', 'Water']; 548 | listRecycled: Array = []; 549 | } 550 | ``` 551 | 552 | #### 12. Simple sortable With Drop into something, without delete it 553 | Here is an example of simple sortable list of items copying in target container: 554 | 555 | ```js 556 | import {Component} from '@angular/core'; 557 | 558 | @Component({ 559 | selector: 'simple-sortable-copy', 560 | template: ` 561 |

Simple sortable With Drop into something, without delete it

562 |
563 |
564 |
566 |
Source List
567 |
568 |
    569 |
  • {{source.name}}
  • 572 |
573 |
574 |
575 |
576 |
577 |
578 |
Target List
579 |
580 |
    581 |
  • 582 | {{target.name}} 583 |
  • 584 |
585 |
586 |
587 |
588 |
` 589 | }) 590 | export class SimpleSortableCopyComponent { 591 | 592 | sourceList: Widget[] = [ 593 | new Widget('1'), new Widget('2'), 594 | new Widget('3'), new Widget('4'), 595 | new Widget('5'), new Widget('6') 596 | ]; 597 | 598 | targetList: Widget[] = []; 599 | addTo($event: any) { 600 | this.targetList.push($event.dragData); 601 | } 602 | } 603 | 604 | class Widget { 605 | constructor(public name: string) {} 606 | } 607 | ``` 608 | 609 | #### 13. Multi list sortable between containers 610 | Here is an example of multi list sortable of boxers moving in container and between containers via drag and drop operation: 611 | 612 | ```js 613 | import {Component} from '@angular/core'; 614 | 615 | @Component({ 616 | selector: 'embedded-sortable', 617 | template: ` 618 |

Move items between multi list sortable containers

619 |
620 |
621 | Drag Containers 622 |
623 |
626 |
628 |
629 | {{container.id}} - {{container.name}} 630 |
631 |
632 |
    633 |
  • {{widget.name}}
  • 636 |
637 |
638 |
639 |
640 |
641 |
642 |
643 |
644 |
Widgets
645 |
646 |
647 |
648 | {{widget.name}} 649 |
650 |
651 |
652 |
653 |
654 |
` 655 | }) 656 | export class EmbeddedSortableComponent { 657 | dragOperation: boolean = false; 658 | 659 | containers: Array = [ 660 | new Container(1, 'Container 1', [new Widget('1'), new Widget('2')]), 661 | new Container(2, 'Container 2', [new Widget('3'), new Widget('4')]), 662 | new Container(3, 'Container 3', [new Widget('5'), new Widget('6')]) 663 | ]; 664 | 665 | widgets: Array = []; 666 | addTo($event: any) { 667 | if ($event) { 668 | this.widgets.push($event.dragData); 669 | } 670 | } 671 | } 672 | 673 | class Container { 674 | constructor(public id: number, public name: string, public widgets: Array) {} 675 | } 676 | 677 | class Widget { 678 | constructor(public name: string) {} 679 | } 680 | ``` 681 | 682 | #### 14. Simple FormArray sortable with Drag-and-Drop 683 | Here is an example of simple sortable of favorite drinks moving in container via drag and drop operation but using FormArray instead of Array: 684 | 685 | ```js 686 | import {Component} from '@angular/core'; 687 | import {FormArray, FormControl} from '@angular/forms'; 688 | 689 | @Component({ 690 | selector: 'simple-formarray-sortable', 691 | template: ` 692 |

Simple FormArray sortable

693 |
694 |
695 |
696 |
697 | Favorite drinks 698 |
699 |
700 |
    701 |
  • 702 |
703 |
704 |
705 |
706 |
707 |
708 |
709 | My prefences:
710 | {{i + 1}}) {{item.value}}
711 |
712 |
713 |
714 |
` 715 | }) 716 | export class SimpleFormArraySortableComponent { 717 | listOne: FormArray = new FormArray([ 718 | new FormControl('Coffee'), 719 | new FormControl('Orange Juice'), 720 | new FormControl('Red Wine'), 721 | new FormControl('Unhealty drink!'), 722 | new FormControl('Water') 723 | ]); 724 | } 725 | ``` 726 | 727 | ## How to pass multiple data in dragData while dragging ? 728 | 729 | 1) As an array: 730 | 731 | ``` html 732 | [dragData]="[aComponent,'component-in-bar']" 733 | ``` 734 | 735 | ``` javascript 736 | loadComponent($event){ 737 | console.log($event.dragData[0]); // aComponent 738 | console.log($event.dragData[1]); // 'component-in-bar' OR 'component-in-designer' 739 | } 740 | ``` 741 | 742 | 2) As an object: 743 | 744 | ``` html 745 | [dragData]="{component: aComponent, location: 'component-in-bar'}" 746 | ``` 747 | 748 | ``` javascript 749 | loadComponent($event){ 750 | console.log($event.dragData.component); // aComponent 751 | console.log($event.dragData.location); // 'component-in-bar' OR 'component-in-designer' 752 | } 753 | ``` 754 | 755 | # Retreiving files in a drop zone 756 | 757 | Since it is possible to drag and drop one or more files to a drop zone, you need to handle the incoming files. 758 | 759 | ```js 760 | import {Component} from '@angular/core'; 761 | import {Http, Headers} from '@angular/http'; 762 | import {DND_PROVIDERS, DND_DIRECTIVES} from 'ng2-dnd/ng2-dnd'; 763 | import {bootstrap} from '@angular/platform-browser-dynamic'; 764 | 765 | bootstrap(AppComponent, [ 766 | DND_PROVIDERS // It is required to have 1 unique instance of your service 767 | ]); 768 | 769 | @Component({ 770 | selector: 'app', 771 | directives: [DND_DIRECTIVES], 772 | template: ` 773 |

Simple Drag-and-Drop

774 |
775 | 776 |
777 |
> 779 |
Place to drop
780 |
781 |
782 |
783 |
784 |
785 | ` 786 | }) 787 | export class AppComponent { 788 | 789 | constructor(private _http: Http) { } 790 | 791 | /** 792 | * The $event is a structure: 793 | * { 794 | * dragData: any, 795 | * mouseEvent: MouseEvent 796 | * } 797 | */ 798 | transferDataSuccess($event) { 799 | // let attachmentUploadUrl = 'assets/data/offerspec/offerspec.json'; 800 | // loading the FileList from the dataTransfer 801 | let dataTransfer: DataTransfer = $event.mouseEvent.dataTransfer; 802 | if (dataTransfer && dataTransfer.files) { 803 | 804 | // needed to support posting binaries and usual form values 805 | let headers = new Headers(); 806 | headers.append('Content-Type', 'multipart/form-data'); 807 | 808 | let files: FileList = dataTransfer.files; 809 | 810 | // uploading the files one by one asynchrounusly 811 | for (let i = 0; i < files.length; i++) { 812 | let file: File = files[i]; 813 | 814 | // just for debugging 815 | console.log('Name: ' + file.name + '\n Type: ' + file.type + '\n Size: ' + file.size + '\n Date: ' + file.lastModifiedDate); 816 | 817 | // collecting the data to post 818 | var data = new FormData(); 819 | data.append('file', file); 820 | data.append('fileName', file.name); 821 | data.append('fileSize', file.size); 822 | data.append('fileType', file.type); 823 | data.append('fileLastMod', file.lastModifiedDate); 824 | 825 | // posting the data 826 | this._http 827 | .post(attachmentUploadUrl, data, { 828 | headers: headers 829 | }) 830 | .toPromise() 831 | .catch(reason => { 832 | console.log(JSON.stringify(reason)); 833 | }); 834 | } 835 | } 836 | } 837 | } 838 | 839 | # Credits 840 | - [Francesco Cina](https://github.com/ufoscout) 841 | - [Valerii Kuznetsov](https://github.com/solival) 842 | - [Shane Oborn](https://github.com/obosha) 843 | - [Juergen Gutsch](https://github.com/JuergenGutsch) 844 | - [Damjan Cilenšek](https://github.com/loudandwicked) 845 | 846 | # License 847 | [MIT](/LICENSE) 848 | -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * taken from angular2-webpack-starter 3 | */ 4 | var path = require('path'); 5 | 6 | // Helper functions 7 | var ROOT = path.resolve(__dirname, '..'); 8 | 9 | function hasProcessFlag(flag) { 10 | return process.argv.join('').indexOf(flag) > -1; 11 | } 12 | 13 | function isWebpackDevServer() { 14 | return process.argv[1] && !! (/webpack-dev-server$/.exec(process.argv[1])); 15 | } 16 | 17 | function root(args) { 18 | args = Array.prototype.slice.call(arguments, 0); 19 | return path.join.apply(path, [ROOT].concat(args)); 20 | } 21 | 22 | function checkNodeImport(context, request, cb) { 23 | if (!path.isAbsolute(request) && request.charAt(0) !== '.') { 24 | cb(null, 'commonjs ' + request); return; 25 | } 26 | cb(); 27 | } 28 | 29 | exports.hasProcessFlag = hasProcessFlag; 30 | exports.isWebpackDevServer = isWebpackDevServer; 31 | exports.root = root; 32 | exports.checkNodeImport = checkNodeImport; -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | var testWebpackConfig = require('./webpack.test.js'); 3 | 4 | var configuration = { 5 | basePath: '', 6 | 7 | frameworks: ['jasmine'], 8 | 9 | // list of files to exclude 10 | exclude: [ ], 11 | 12 | /* 13 | * list of files / patterns to load in the browser 14 | * 15 | * we are building the test environment in ./spec-bundle.js 16 | */ 17 | files: [ { pattern: './config/spec-bundle.js', watched: false } ], 18 | 19 | preprocessors: { './config/spec-bundle.js': ['coverage', 'webpack', 'sourcemap'] }, 20 | 21 | // Webpack Config at ./webpack.test.js 22 | webpack: testWebpackConfig, 23 | 24 | coverageReporter: { 25 | type: 'in-memory' 26 | }, 27 | 28 | remapCoverageReporter: { 29 | 'text-summary': null, 30 | json: './coverage/coverage.json', 31 | html: './coverage/html' 32 | }, 33 | 34 | // Webpack please don't spam the console when running in karma! 35 | webpackMiddleware: { stats: 'errors-only'}, 36 | 37 | reporters: [ 'mocha', 'coverage', 'remap-coverage' ], 38 | 39 | // web server port 40 | port: 9876, 41 | 42 | colors: true, 43 | 44 | /* 45 | * level of logging 46 | * possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 47 | */ 48 | logLevel: config.LOG_INFO, 49 | 50 | autoWatch: false, 51 | 52 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 53 | 54 | singleRun: true 55 | }; 56 | 57 | config.set(configuration); 58 | }; -------------------------------------------------------------------------------- /config/spec-bundle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * When testing with webpack and ES6, we have to do some extra 3 | * things to get testing to work right. Because we are gonna write tests 4 | * in ES6 too, we have to compile those as well. That's handled in 5 | * karma.conf.js with the karma-webpack plugin. This is the entry 6 | * file for webpack test. Just like webpack will create a bundle.js 7 | * file for our client, when we run test, it will compile and bundle them 8 | * all here! Crazy huh. So we need to do some setup 9 | */ 10 | Error.stackTraceLimit = Infinity; 11 | 12 | require('core-js/es6'); 13 | require('core-js/es7/reflect'); 14 | 15 | // Typescript emit helpers polyfill 16 | require('ts-helpers'); 17 | 18 | require('zone.js/dist/zone'); 19 | require('zone.js/dist/long-stack-trace-zone'); 20 | require('zone.js/dist/async-test'); 21 | require('zone.js/dist/fake-async-test'); 22 | require('zone.js/dist/sync-test'); 23 | require('zone.js/dist/proxy'); // since zone.js 0.6.15 24 | require('zone.js/dist/jasmine-patch'); // put here since zone.js 0.6.14 25 | 26 | // RxJS 27 | require('rxjs/Rx'); 28 | 29 | var testing = require('@angular/core/testing'); 30 | var browser = require('@angular/platform-browser-dynamic/testing'); 31 | 32 | testing.TestBed.initTestEnvironment( 33 | browser.BrowserDynamicTestingModule, 34 | browser.platformBrowserDynamicTesting() 35 | ); 36 | 37 | /* 38 | * Ok, this is kinda crazy. We can use the context method on 39 | * require that webpack created in order to tell webpack 40 | * what files we actually want to require or import. 41 | * Below, context will be a function/object with file names as keys. 42 | * Using that regex we are saying look in ../src then find 43 | * any file that ends with spec.ts and get its path. By passing in true 44 | * we say do this recursively 45 | */ 46 | var testContext = require.context('../tests', true, /\.spec\.ts/); 47 | 48 | /* 49 | * get all the files, for each file, call the context function 50 | * that will require the file and load it up here. Context will 51 | * loop and require those spec files here 52 | */ 53 | function requireAll(requireContext) { 54 | return requireContext.keys().map(requireContext); 55 | } 56 | 57 | // requires and returns all modules that match 58 | var modules = requireAll(testContext); -------------------------------------------------------------------------------- /config/testing-utils.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* 4 | Temporary fiile for referencing the TypeScript defs for Jasmine + some potentially 5 | utils for testing. Will change/adjust this once I find a better way of doing 6 | */ 7 | 8 | declare module jasmine { 9 | interface Matchers { 10 | toHaveText(text: string): boolean; 11 | toContainText(text: string): boolean; 12 | } 13 | } 14 | 15 | beforeEach(() => { 16 | jasmine.addMatchers({ 17 | 18 | toHaveText: function() { 19 | return { 20 | compare: function(actual, expectedText) { 21 | var actualText = actual.textContent; 22 | return { 23 | pass: actualText === expectedText, 24 | get message() { 25 | return 'Expected ' + actualText + ' to equal ' + expectedText; 26 | } 27 | }; 28 | } 29 | }; 30 | }, 31 | 32 | toContainText: function() { 33 | return { 34 | compare: function(actual, expectedText) { 35 | var actualText = actual.textContent; 36 | return { 37 | pass: actualText.indexOf(expectedText) > -1, 38 | get message() { 39 | return 'Expected ' + actualText + ' to contain ' + expectedText; 40 | } 41 | }; 42 | } 43 | }; 44 | } 45 | }); 46 | }); -------------------------------------------------------------------------------- /config/webpack.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from angular2-webpack-starter 3 | */ 4 | 5 | const helpers = require('./helpers'), 6 | webpack = require('webpack'), 7 | LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); 8 | 9 | /** 10 | * Webpack Plugins 11 | */ 12 | 13 | module.exports = { 14 | 15 | /** 16 | * Source map for Karma from the help of karma-sourcemap-loader & karma-webpack 17 | * 18 | * Do not change, leave as is or it wont work. 19 | * See: https://github.com/webpack/karma-webpack#source-maps 20 | */ 21 | devtool: 'inline-source-map', 22 | 23 | resolve: { 24 | extensions: ['.ts', '.js'], 25 | modules: [helpers.root('src'), 'node_modules'] 26 | }, 27 | 28 | module: { 29 | rules: [{ 30 | enforce: 'pre', 31 | test: /\.ts$/, 32 | loader: 'tslint-loader', 33 | exclude: [helpers.root('node_modules')] 34 | }, { 35 | enforce: 'pre', 36 | test: /\.js$/, 37 | loader: 'source-map-loader', 38 | exclude: [ 39 | // these packages have problems with their sourcemaps 40 | helpers.root('node_modules/rxjs'), 41 | helpers.root('node_modules/@angular') 42 | ] 43 | }, { 44 | test: /\.ts$/, 45 | loader: 'awesome-typescript-loader', 46 | query: { 47 | // use inline sourcemaps for "karma-remap-coverage" reporter 48 | sourceMap: false, 49 | inlineSourceMap: true, 50 | module: "commonjs", 51 | removeComments: true 52 | }, 53 | exclude: [/\.e2e\.ts$/] 54 | }, { 55 | enforce: 'post', 56 | test: /\.(js|ts)$/, 57 | loader: 'istanbul-instrumenter-loader', 58 | include: helpers.root('src'), 59 | exclude: [/\.spec\.ts$/, /\.e2e\.ts$/, /node_modules/] 60 | }], 61 | }, 62 | 63 | plugins: [ 64 | // fix the warning in ./~/@angular/core/src/linker/system_js_ng_module_factory_loader.js 65 | new webpack.ContextReplacementPlugin( 66 | /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/, 67 | helpers.root('./src') 68 | ), 69 | 70 | new LoaderOptionsPlugin({ 71 | debug: true, 72 | options: { 73 | 74 | /** 75 | * Static analysis linter for TypeScript advanced options configuration 76 | * Description: An extensible linter for the TypeScript language. 77 | * 78 | * See: https://github.com/wbuchwalter/tslint-loader 79 | */ 80 | 'tslint-loader': { 81 | emitErrors: false, 82 | failOnHint: false, 83 | resourcePath: 'src' 84 | }, 85 | 86 | } 87 | }) 88 | ] 89 | }; 90 | -------------------------------------------------------------------------------- /demo/.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "demo" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "app", 21 | "styles": [ 22 | "../node_modules/ng2-dnd/style.css", 23 | "styles.scss" 24 | ], 25 | "scripts": [], 26 | "environmentSource": "environments/environment.ts", 27 | "environments": { 28 | "dev": "environments/environment.ts", 29 | "prod": "environments/environment.prod.ts" 30 | } 31 | } 32 | ], 33 | "e2e": { 34 | "protractor": { 35 | "config": "./protractor.conf.js" 36 | } 37 | }, 38 | "lint": [ 39 | { 40 | "project": "src/tsconfig.app.json" 41 | }, 42 | { 43 | "project": "src/tsconfig.spec.json" 44 | }, 45 | { 46 | "project": "e2e/tsconfig.e2e.json" 47 | } 48 | ], 49 | "test": { 50 | "karma": { 51 | "config": "./karma.conf.js" 52 | } 53 | }, 54 | "defaults": { 55 | "styleExt": "css", 56 | "component": {} 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /demo/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.0.1. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | Before running the tests make sure you are serving the app via `ng serve`. 25 | 26 | ## Further help 27 | 28 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 29 | -------------------------------------------------------------------------------- /demo/e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { DemoPage } from './app.po'; 2 | 3 | describe('demo App', () => { 4 | let page: DemoPage; 5 | 6 | beforeEach(() => { 7 | page = new DemoPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /demo/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class DemoPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types":[ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /demo/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | files: [ 19 | { pattern: './src/test.ts', watched: false } 20 | ], 21 | preprocessors: { 22 | './src/test.ts': ['@angular/cli'] 23 | }, 24 | mime: { 25 | 'text/x-typescript': ['ts','tsx'] 26 | }, 27 | coverageIstanbulReporter: { 28 | reports: [ 'html', 'lcovonly' ], 29 | fixWebpackSourcePaths: true 30 | }, 31 | angularCli: { 32 | environment: 'dev' 33 | }, 34 | reporters: config.angularCli && config.angularCli.codeCoverage 35 | ? ['progress', 'coverage-istanbul'] 36 | : ['progress', 'kjhtml'], 37 | port: 9876, 38 | colors: true, 39 | logLevel: config.LOG_INFO, 40 | autoWatch: true, 41 | browsers: ['Chrome'], 42 | singleRun: false 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/common": "^4.0.0", 16 | "@angular/compiler": "^4.0.0", 17 | "@angular/core": "^4.0.0", 18 | "@angular/forms": "^4.0.0", 19 | "@angular/http": "^4.0.0", 20 | "@angular/platform-browser": "^4.0.0", 21 | "@angular/platform-browser-dynamic": "^4.0.0", 22 | "@angular/router": "^4.0.0", 23 | "angular-prism": "^0.1.20", 24 | "core-js": "^2.4.1", 25 | "ng2-dnd": "file:..", 26 | "rxjs": "^5.1.0", 27 | "zone.js": "^0.8.4" 28 | }, 29 | "devDependencies": { 30 | "@angular/cli": "1.0.1", 31 | "@angular/compiler-cli": "^4.0.0", 32 | "@types/jasmine": "2.5.38", 33 | "@types/node": "~6.0.60", 34 | "codelyzer": "~2.0.0", 35 | "jasmine-core": "~2.5.2", 36 | "jasmine-spec-reporter": "~3.2.0", 37 | "karma": "~1.4.1", 38 | "karma-chrome-launcher": "~2.0.0", 39 | "karma-cli": "~1.0.1", 40 | "karma-jasmine": "~1.1.0", 41 | "karma-jasmine-html-reporter": "^0.2.2", 42 | "karma-coverage-istanbul-reporter": "^0.2.0", 43 | "protractor": "~5.1.0", 44 | "ts-node": "~2.0.0", 45 | "tslint": "~4.5.0", 46 | "typescript": "~2.2.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /demo/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akserg/ng2-dnd/f13b207851021a4478e66bfdb1d874c2c3f116b9/demo/src/app/app.component.css -------------------------------------------------------------------------------- /demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 | -------------------------------------------------------------------------------- /demo/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [ 9 | AppComponent 10 | ], 11 | }).compileComponents(); 12 | })); 13 | 14 | it('should create the app', async(() => { 15 | const fixture = TestBed.createComponent(AppComponent); 16 | const app = fixture.debugElement.componentInstance; 17 | expect(app).toBeTruthy(); 18 | })); 19 | 20 | it(`should have as title 'app works!'`, async(() => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('app works!'); 24 | })); 25 | 26 | it('should render title in a h1 tag', async(() => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | fixture.detectChanges(); 29 | const compiled = fixture.debugElement.nativeElement; 30 | expect(compiled.querySelector('h1').textContent).toContain('app works!'); 31 | })); 32 | }); 33 | -------------------------------------------------------------------------------- /demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'app works!'; 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpModule } from '@angular/http'; 5 | import { RouterModule } from '@angular/router'; 6 | 7 | /* Import prism core */ 8 | import 'prismjs/prism'; 9 | 10 | /* Import the language you need to highlight */ 11 | import 'prismjs/components/prism-typescript'; 12 | 13 | import { routes } from './app.router'; 14 | import { DndModule } from 'ng2-dnd'; 15 | 16 | import { SharedModule } from './shared'; 17 | import { DemoDndModule } from './examples'; 18 | import { AppComponent } from './app.component'; 19 | 20 | @NgModule({ 21 | declarations: [ 22 | AppComponent 23 | ], 24 | imports: [ 25 | BrowserModule, 26 | FormsModule, 27 | HttpModule, 28 | SharedModule, 29 | RouterModule.forRoot(routes), 30 | DndModule.forRoot(), 31 | DemoDndModule 32 | ], 33 | providers: [], 34 | bootstrap: [AppComponent] 35 | }) 36 | export class AppModule { } 37 | -------------------------------------------------------------------------------- /demo/src/app/app.router.ts: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from '@angular/router'; 2 | 3 | export const routes: Routes = [ 4 | { path: '', loadChildren: './examples/index#DemoDndModule' }, 5 | ]; 6 | -------------------------------------------------------------------------------- /demo/src/app/examples/demo-dnd.router.ts: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from '@angular/router'; 2 | 3 | import { SimpleDemoComponent, DndSimpleComponent } from './dnd/simple'; 4 | import { ZoneComponent } from './dnd/zone/zone.component'; 5 | import { CustomDataComponent } from './dnd/custom-data/custom-data.component'; 6 | import { CustomFunctionComponent } from './dnd/custom-function/custom-function.component'; 7 | import { ShoppingBasketComponent } from './dnd/shopping-basket/shopping-basket.component'; 8 | 9 | import { SimpleComponent } from './sortable/simple/simple.component'; 10 | import { MultiComponent } from './sortable/multi/multi.component'; 11 | import { RecycleMultiComponent } from './sortable/recycle-multi/recycle-multi.component'; 12 | import { EmbeddedComponent} from './sortable/embedded/embedded.component'; 13 | import { SimpleSortableCopyComponent } from './sortable/simple-sortable-copy/simple-sortable-copy.component'; 14 | 15 | export const dndComponents = [SimpleDemoComponent, SimpleComponent, ZoneComponent, CustomDataComponent, CustomFunctionComponent, ShoppingBasketComponent]; 16 | export const sortableComponents = [SimpleComponent, MultiComponent, RecycleMultiComponent, EmbeddedComponent, SimpleSortableCopyComponent]; 17 | 18 | export const routes: Routes = [ 19 | { path: '', pathMatch: 'full', redirectTo: 'dnd-simple' }, 20 | 21 | { path: 'dnd-simple', component: SimpleDemoComponent }, 22 | { path: 'dnd-zone', component: ZoneComponent }, 23 | { path: 'dnd-custom-data', component: CustomDataComponent }, 24 | { path: 'dnd-custom-function', component: CustomFunctionComponent }, 25 | { path: 'dnd-shopping-basket', component: ShoppingBasketComponent }, 26 | 27 | { path: 'sortable-simple', component: SimpleComponent }, 28 | { path: 'sortable-recycle-multi', component: RecycleMultiComponent }, 29 | { path: 'sortable-simple-copy', component: SimpleSortableCopyComponent }, 30 | { path: 'sortable-multi', component: MultiComponent }, 31 | { path: 'sortable-embedded', component: EmbeddedComponent } 32 | ]; 33 | -------------------------------------------------------------------------------- /demo/src/app/examples/dnd/custom-data/custom-data.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'custom-data', 5 | template: ` 6 |

Transfer custom data in Drag-and-Drop

7 |
8 |
9 |
10 |
Available to drag
11 |
12 |
13 |
14 |
Drag Me
15 |
{{transferData | json}}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
Place to drop (Items:{{receivedData.length}})
24 |
25 |
{{data | json}}
26 |
27 |
28 |
29 |
` 30 | }) 31 | export class CustomDataComponent { 32 | transferData: Object = {id: 1, msg: 'Hello'}; 33 | receivedData: Array = []; 34 | 35 | transferDataSuccess($event: any) { 36 | this.receivedData.push($event); 37 | } 38 | } -------------------------------------------------------------------------------- /demo/src/app/examples/dnd/custom-function/custom-function.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'custom-function', 5 | template: ` 6 |

Use a custom function to determine where dropping is allowed

7 |
8 |
9 |
10 |
Available to drag
11 |
12 |
13 |
dragData = 6
14 |
15 |
16 |
dragData = 10
17 |
18 |
19 |
dragData = 30
20 |
21 |
22 |
23 |
24 |
25 |
allowDropFunction(baseInteger: any): any {{ '{' }}
26 |   return (dragData: any) => dragData % baseInteger === 0;
27 | {{ '}' }}
28 |
29 |
30 |
31 |
32 | Multiples of 33 | 34 | only 35 |
36 |
37 |
dragData = {{item}}
38 |
39 |
40 |
41 |
42 |
43 |
44 | Multiples of 45 | 46 | only 47 |
48 |
49 |
dragData = {{item}}
50 |
51 |
52 |
53 |
54 |
55 |
56 | ` 57 | }) 58 | export class CustomFunctionComponent { 59 | box1Integer: number = 3; 60 | box2Integer: number = 10; 61 | 62 | box1Items: string[] = []; 63 | box2Items: string[] = []; 64 | 65 | allowDropFunction(baseInteger: number): any { 66 | return (dragData: any) => dragData % baseInteger === 0; 67 | } 68 | 69 | addTobox1Items($event: any) { 70 | this.box1Items.push($event.dragData); 71 | } 72 | 73 | addTobox2Items($event: any) { 74 | this.box2Items.push($event.dragData); 75 | } 76 | } -------------------------------------------------------------------------------- /demo/src/app/examples/dnd/shopping-basket/shopping-basket.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'shoping-basket', 5 | template: ` 6 |

Drag-and-Drop - Shopping basket

7 |
8 | 9 |
10 |
11 |
Available products
12 |
13 |
15 |
16 |
{{product.name}} - \${{product.cost}}
(available: {{product.quantity}})
17 |
{{product.name}}
(NOT available)
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Shopping Basket
(to pay: \${{totalCost()}})
26 |
27 |
28 |
29 | {{product.name}}
(ordered: {{product.quantity}}
cost: \${{product.cost * product.quantity}}) 30 |
31 |
32 |
33 |
34 |
35 |
` 36 | }) 37 | export class ShoppingBasketComponent { 38 | availableProducts: Array = []; 39 | shoppingBasket: Array = []; 40 | 41 | constructor() { 42 | this.availableProducts.push(new Product('Blue Shoes', 3, 35)); 43 | this.availableProducts.push(new Product('Good Jacket', 1, 90)); 44 | this.availableProducts.push(new Product('Red Shirt', 5, 12)); 45 | this.availableProducts.push(new Product('Blue Jeans', 4, 60)); 46 | } 47 | 48 | orderedProduct($event: any) { 49 | let orderedProduct: Product = $event.dragData; 50 | orderedProduct.quantity--; 51 | } 52 | 53 | addToBasket($event: any) { 54 | let newProduct: Product = $event.dragData; 55 | for (let indx in this.shoppingBasket) { 56 | let product: Product = this.shoppingBasket[indx]; 57 | if (product.name === newProduct.name) { 58 | product.quantity++; 59 | return; 60 | } 61 | } 62 | this.shoppingBasket.push(new Product(newProduct.name, 1, newProduct.cost)); 63 | this.shoppingBasket.sort((a: Product, b: Product) => { 64 | return a.name.localeCompare(b.name); 65 | }); 66 | } 67 | 68 | totalCost(): number { 69 | let cost: number = 0; 70 | for (let indx in this.shoppingBasket) { 71 | let product: Product = this.shoppingBasket[indx]; 72 | cost += (product.cost * product.quantity); 73 | } 74 | return cost; 75 | } 76 | } 77 | 78 | class Product { 79 | constructor(public name: string, public quantity: number, public cost: number) {} 80 | } -------------------------------------------------------------------------------- /demo/src/app/examples/dnd/simple/index.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | declare var require: any; 4 | 5 | @Component({ 6 | template: ` 7 | 8 |
9 |
10 |
11 |
The source code
12 |
13 | 14 |
15 |
16 |
17 |
` 18 | }) 19 | export class SimpleDemoComponent { 20 | 21 | tsCode: string = require('!!raw-loader!./simple.component'); 22 | } 23 | 24 | export { DndSimpleComponent } from './simple.component'; -------------------------------------------------------------------------------- /demo/src/app/examples/dnd/simple/simple.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'dnd-simple', 5 | template: ` 6 |

Simple Drag-and-Drop

7 |
8 |
9 |
10 |
Available to drag
11 |
12 | 13 |
15 |
16 |

Drag Me

17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
Place to drop
25 |
27 |

Dropped {{simpleDrop}} times

28 |
29 |
30 |
31 |
` 32 | }) 33 | export class DndSimpleComponent { 34 | simpleDrop: number = 0; 35 | } -------------------------------------------------------------------------------- /demo/src/app/examples/dnd/zone/zone.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'zone', 5 | template: ` 6 |

Restricted Drag-and-Drop with zones

7 |
8 |
9 |
10 |
Available to drag
11 |
12 |
13 |
14 |
Drag Me
15 |
Zone 1 only
16 |
17 |
18 |
19 |
20 | 21 |
22 |
Available to drag
23 |
24 |
25 |
26 |
Drag Me
27 |
Zone 1 & 2
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
Zone 1
36 |
37 |
Item was dropped here
38 |
39 |
40 |
41 |
42 |
43 |
Zone 2
44 |
45 |
Item was dropped here
46 |
47 |
48 |
49 |
` 50 | }) 51 | export class ZoneComponent { 52 | restrictedDrop1: any = null; 53 | restrictedDrop2: any = null; 54 | } -------------------------------------------------------------------------------- /demo/src/app/examples/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 Sergey Akopkokhyants 2 | // This project is licensed under the terms of the MIT license. 3 | // https://github.com/akserg 4 | 5 | import { NgModule } from '@angular/core'; 6 | import { CommonModule } from '@angular/common'; 7 | import { FormsModule } from '@angular/forms'; 8 | import { RouterModule } from '@angular/router'; 9 | 10 | import { PrismComponent } from 'angular-prism'; 11 | 12 | import { routes, dndComponents, sortableComponents } from './demo-dnd.router'; 13 | import { DndModule } from 'ng2-dnd'; 14 | 15 | @NgModule({ 16 | imports: [CommonModule, FormsModule, DndModule.forRoot(), RouterModule.forChild(routes)], 17 | declarations: [PrismComponent, ...dndComponents, ...sortableComponents], 18 | exports: [...dndComponents, ...sortableComponents] 19 | }) 20 | export class DemoDndModule { } -------------------------------------------------------------------------------- /demo/src/app/examples/sortable/embedded/embedded.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'embedded', 5 | template: ` 6 |

Move items between multi list sortable containers

7 |
8 |
9 | Drag Containers 10 |
11 |
14 |
16 |
17 | {{container.id}} - {{container.name}} 18 |
19 |
20 |
    21 |
  • {{widget.name}}
  • 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
Widgets
33 |
34 |
35 |
36 | {{widget.name}} 37 |
38 |
39 |
40 |
41 |
42 |
` 43 | }) 44 | export class EmbeddedComponent { 45 | dragOperation: boolean = false; 46 | 47 | containers: Array = [ 48 | new Container(1, 'Container 1', [new Widget('1'), new Widget('2')]), 49 | new Container(2, 'Container 2', [new Widget('3'), new Widget('4')]), 50 | new Container(3, 'Container 3', [new Widget('5'), new Widget('6')]) 51 | ]; 52 | 53 | widgets: Array = []; 54 | addTo($event: any) { 55 | if ($event) { 56 | this.widgets.push($event.dragData); 57 | } 58 | } 59 | } 60 | 61 | class Container { 62 | constructor(public id: number, public name: string, public widgets: Array) {} 63 | } 64 | 65 | class Widget { 66 | constructor(public name: string) {} 67 | } -------------------------------------------------------------------------------- /demo/src/app/examples/sortable/multi/multi.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'multi', 5 | template: ` 6 |

Multi list sortable

7 |
8 |
9 |
10 |
11 | Available boxers 12 |
13 |
14 |
    15 |
  • {{item}}
  • 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | First Team 24 |
25 |
26 |
    27 |
  • {{item}}
  • 28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Second Team 36 |
37 |
38 |
    39 |
  • {{item}}
  • 40 |
41 |
42 |
43 |
44 |
` 45 | }) 46 | export class MultiComponent { 47 | listBoxers: Array = ['Sugar Ray Robinson', 'Muhammad Ali', 'George Foreman', 'Joe Frazier', 'Jake LaMotta', 'Joe Louis', 'Jack Dempsey', 'Rocky Marciano', 'Mike Tyson', 'Oscar De La Hoya']; 48 | listTeamOne: Array = []; 49 | listTeamTwo: Array = []; 50 | } -------------------------------------------------------------------------------- /demo/src/app/examples/sortable/recycle-multi/recycle-multi.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'recycle-multi', 5 | template: ` 6 |

Simple sortable With Drop into recycle bin

7 |
8 |
9 |
10 |
11 | Favorite drinks 12 |
13 |
14 |
    15 |
  • {{item}}
  • 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | Recycle bin: Drag into me to delete it
25 |
26 |
27 |
28 | Recycled: {{listRecycled.toString()}} 29 |
30 |
31 |
` 32 | }) 33 | export class RecycleMultiComponent { 34 | listOne: Array = ['Coffee', 'Orange Juice', 'Red Wine', 'Unhealty drink!', 'Water']; 35 | listRecycled: Array = []; 36 | } -------------------------------------------------------------------------------- /demo/src/app/examples/sortable/simple-sortable-copy/simple-sortable-copy.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'simple-sortable-copy', 5 | template: ` 6 |

Simple sortable With Drop into something, without delete it

7 |
8 |
9 |
11 |
Source List
12 |
13 |
    14 |
  • {{source.name}}
  • 17 |
18 |
19 |
20 |
21 |
22 |
23 |
Target List
24 |
25 |
    26 |
  • 27 | {{target.name}} 28 |
  • 29 |
30 |
31 |
32 |
33 |
` 34 | }) 35 | export class SimpleSortableCopyComponent { 36 | 37 | sourceList: Widget[] = [ 38 | new Widget('1'), new Widget('2'), 39 | new Widget('3'), new Widget('4'), 40 | new Widget('5'), new Widget('6') 41 | ]; 42 | 43 | targetList: Widget[] = []; 44 | addTo($event: any) { 45 | this.targetList.push($event.dragData); 46 | } 47 | } 48 | 49 | class Widget { 50 | constructor(public name: string) {} 51 | } -------------------------------------------------------------------------------- /demo/src/app/examples/sortable/simple/simple.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'simple', 5 | template: ` 6 |

Simple sortable

7 |
8 |
9 |
10 |
11 | Favorite drinks 12 |
13 |
14 |
    15 |
  • {{item}}
  • 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | My prefences:
24 | {{i + 1}}) {{item}}
25 |
26 |
27 |
28 |
` 29 | }) 30 | export class SimpleComponent { 31 | listOne: Array = ['Coffee', 'Orange Juice', 'Red Wine', 'Unhealty drink!', 'Water']; 32 | } -------------------------------------------------------------------------------- /demo/src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {RouterModule} from '@angular/router'; 4 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 5 | import {JsonpModule} from '@angular/http'; 6 | 7 | // import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; 8 | 9 | // import {ComponentWrapper} from './component-wrapper/component-wrapper.component'; 10 | // import {PageWrapper} from './page-wrapper/page-wrapper.component'; 11 | import {SideNavComponent} from './side-nav/side-nav.component'; 12 | // import {Analytics} from './analytics/analytics'; 13 | 14 | export {componentsList} from './side-nav/side-nav.component'; 15 | 16 | @NgModule({ 17 | imports: [CommonModule, RouterModule], 18 | exports: [ 19 | CommonModule, 20 | RouterModule, 21 | // ComponentWrapper, 22 | // PageWrapper, 23 | SideNavComponent, 24 | // NgbModule, 25 | FormsModule, 26 | ReactiveFormsModule, 27 | JsonpModule 28 | ], 29 | declarations: [ 30 | // ComponentWrapper, 31 | // PageWrapper, 32 | SideNavComponent, 33 | ], 34 | // providers: [Analytics] 35 | }) 36 | export class SharedModule { 37 | } -------------------------------------------------------------------------------- /demo/src/app/shared/side-nav/side-nav.component.html: -------------------------------------------------------------------------------- 1 |
2 | 15 |
-------------------------------------------------------------------------------- /demo/src/app/shared/side-nav/side-nav.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import {Router} from '@angular/router'; 3 | 4 | export const componentsList = [ 5 | 'Accordion', 6 | 'Alert', 7 | 'Buttons', 8 | 'Carousel', 9 | 'Collapse', 10 | 'Datepicker', 11 | 'Dropdown', 12 | 'Modal', 13 | 'Pagination', 14 | 'Popover', 15 | 'Progressbar', 16 | 'Rating', 17 | 'Tabs', 18 | 'Timepicker', 19 | 'Tooltip', 20 | 'Typeahead' 21 | ]; 22 | 23 | @Component({ 24 | selector: 'side-nav', 25 | templateUrl: './side-nav.component.html', 26 | }) 27 | export class SideNavComponent { 28 | @Input() activeTab: String; 29 | components = componentsList; 30 | 31 | constructor(private router: Router) {} 32 | 33 | isActive(currentRoute: any[]): boolean { 34 | return this.router.isActive(this.router.createUrlTree(currentRoute), true); 35 | } 36 | } -------------------------------------------------------------------------------- /demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akserg/ng2-dnd/f13b207851021a4478e66bfdb1d874c2c3f116b9/demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /demo/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akserg/ng2-dnd/f13b207851021a4478e66bfdb1d874c2c3f116b9/demo/src/favicon.ico -------------------------------------------------------------------------------- /demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Loading... 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /demo/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | -------------------------------------------------------------------------------- /demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | // styles in src/style directory are applied to the whole page 2 | 3 | .navbar { 4 | background: rgba(255,255,255,0.95); 5 | } 6 | 7 | .bd-booticon { 8 | margin: 1.25rem; 9 | width: 256px; 10 | height: 256px; 11 | } 12 | 13 | .bd-masthead, .jumbotron { 14 | background-color: #0143A3; 15 | background: linear-gradient(135deg, #0143A3, #0273D4); 16 | } 17 | 18 | .bd-masthead { 19 | padding-top: 2rem; 20 | padding-bottom: 2rem; 21 | margin-bottom: 4rem; 22 | text-align: center; 23 | color: #efefef; 24 | 25 | .lead { 26 | margin-right: auto; 27 | margin-bottom: 2rem; 28 | margin-left: auto; 29 | width: 80%; 30 | font-size: 2rem; 31 | color: #fff; 32 | } 33 | 34 | .btn { 35 | color: #fff; 36 | border-color: #fff; 37 | &:hover { 38 | background-color: #f7f7f7; 39 | color: #0273D4; 40 | } 41 | } 42 | } 43 | 44 | .jumbotron { 45 | color: #fff; 46 | border-radius: 0; 47 | } 48 | 49 | .github-buttons { 50 | 51 | header & { 52 | margin-bottom: 0; 53 | margin-top: 4px; 54 | padding-left: 0; 55 | } 56 | 57 | ngbd-default & { 58 | text-align: center; 59 | margin-top: 2rem; 60 | } 61 | } 62 | 63 | @media (min-width: 768px) { 64 | .bd-sidebar { 65 | padding-left: 1rem; 66 | 67 | display: inline-block; 68 | position: -webkit-sticky; 69 | position: sticky; 70 | top: 1rem; 71 | } 72 | } 73 | 74 | .bd-toc-link { 75 | display: block; 76 | padding: .25rem .75rem; 77 | color: #55595c; 78 | } 79 | 80 | .bd-toc-link:focus, 81 | .bd-toc-link:hover { 82 | color: #0275d8; 83 | text-decoration: none; 84 | } 85 | 86 | .active > .bd-toc-link { 87 | font-weight: 500; 88 | color: #373a3c; 89 | } 90 | 91 | .bd-toc-item.active { 92 | margin-top: 1rem; 93 | margin-bottom: 1rem; 94 | } 95 | 96 | .bd-toc-item:first-child { 97 | margin-top: 0; 98 | } 99 | 100 | .bd-toc-item:last-child { 101 | margin-bottom: 2rem; 102 | } 103 | 104 | .bd-sidebar .nav > li > a { 105 | display: block; 106 | padding: .25rem .75rem; 107 | font-size: 90%; 108 | color: #99979c; 109 | } 110 | 111 | .bd-sidebar .nav > li > a:focus, 112 | .bd-sidebar .nav > li > a:hover { 113 | color: #0275d8; 114 | text-decoration: none; 115 | background-color: transparent; 116 | } 117 | 118 | .bd-sidebar .nav > .active:focus > a, 119 | .bd-sidebar .nav > .active:hover > a, 120 | .bd-sidebar .nav > .active > a { 121 | font-weight: 500; 122 | color: #373a3c; 123 | background-color: transparent; 124 | } 125 | 126 | div.api-doc-component { 127 | margin-bottom: 3rem; 128 | 129 | > h2, > h3 { 130 | .github-link { 131 | transition: opacity .5s; 132 | opacity: .3; 133 | } 134 | 135 | &:hover { 136 | .github-link { 137 | opacity: 1; 138 | } 139 | & > .title-fragment { 140 | opacity: 1; 141 | } 142 | } 143 | } 144 | 145 | section { 146 | margin-top: 3rem; 147 | h4 { 148 | margin-top: 2rem; 149 | margin-bottom: 1rem; 150 | } 151 | 152 | .meta { 153 | font-size: .8rem; 154 | margin-bottom: 1rem; 155 | > div { 156 | margin-bottom: .5rem; 157 | } 158 | } 159 | } 160 | } 161 | 162 | a.title-fragment { 163 | opacity: 0; 164 | transition: opacity 125ms ease; 165 | line-height: inherit; 166 | position: absolute; 167 | margin-left: -1.2em; 168 | padding-right: 0.5em; 169 | 170 | & > img { 171 | width: 1em; 172 | height: 1em; 173 | } 174 | } 175 | 176 | div.component-demo { 177 | margin-bottom: 3rem; 178 | h2 { 179 | margin-bottom: 1rem; 180 | 181 | .plunker { 182 | opacity: 0.3; 183 | transition: opacity .5s; 184 | } 185 | &:hover { 186 | .plunker { 187 | opacity: 1; 188 | } 189 | } 190 | } 191 | 192 | .tabset-code { 193 | .nav { 194 | margin: 0; 195 | padding: .5rem 1.25rem 0; 196 | font-size: 80%; 197 | 198 | .nav-link.active { 199 | background-color: #f5f2f0; 200 | border-bottom: 1px solid #f5f2f0; 201 | } 202 | 203 | .nav-link:not(.active) { 204 | color: #999; 205 | &:hover { 206 | color: #666; 207 | } 208 | } 209 | } 210 | 211 | .tab-content { 212 | overflow: hidden; 213 | } 214 | 215 | pre { 216 | margin: 0; 217 | } 218 | } 219 | } 220 | 221 | .examples-legend { 222 | font-size: 80%; 223 | } 224 | 225 | .bd-footer { 226 | padding: 3rem 0; 227 | margin-top: 3rem; 228 | font-size: 85%; 229 | background-color: #f7f7f7; 230 | text-align: left; 231 | 232 | p { 233 | margin-bottom: 0; 234 | } 235 | 236 | a { 237 | font-weight: 500; 238 | color: #55595c; 239 | } 240 | } 241 | 242 | ngbd-api-docs, ngbd-api-docs-class, ngbd-api-docs-config { 243 | display: block; 244 | 245 | &:not(:first-child) { 246 | margin-top: 3rem; 247 | border-top: 1px solid #999; 248 | padding-top: 1rem; 249 | } 250 | } 251 | 252 | 253 | 254 | // override prism theme background color to inline it with bootstrap colors 255 | code[class*="language-"], 256 | pre[class*="language-"] { 257 | background-color: #f5f5f5; // same as bootstrap card header 258 | } 259 | 260 | span.token.tag { 261 | font-size: 1em; 262 | padding: 0; 263 | } 264 | 265 | ngbd-component-wrapper, ngbd-page-wrapper { 266 | .jumbotron { 267 | border-radius: 0; 268 | } 269 | } 270 | 271 | .root-nav { 272 | $offset: 73px; 273 | > .nav-tabs { 274 | transform: translateY(-$offset); 275 | 276 | .nav-link:not(.active) { 277 | color: #f9f9f9; 278 | } 279 | 280 | .nav-link:not(.active):hover { 281 | background-color: rgba(255,255,255, 0.15); 282 | border-color: rgba(255,255,255, 0.15); 283 | } 284 | } 285 | 286 | > .tab-content { 287 | transform: translateY(-$offset / 2); 288 | } 289 | } 290 | 291 | @mixin center-nav-tab-on-small-screens() { 292 | .jumbotron > .container { 293 | margin-bottom: 2rem; 294 | h1 { 295 | text-align: center; 296 | } 297 | } 298 | 299 | .root-nav { 300 | ul { 301 | justify-content: center !important; 302 | } 303 | } 304 | } 305 | 306 | @media (max-width: 768px) and (orientation:portrait) { 307 | @include center-nav-tab-on-small-screens(); 308 | } 309 | 310 | @media (max-width: 568px) and (max-height: 320px) { 311 | @include center-nav-tab-on-small-screens(); 312 | } -------------------------------------------------------------------------------- /demo/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /demo/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /demo/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2016", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true, "rxjs"], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": false, 47 | "no-empty-interface": true, 48 | "no-eval": true, 49 | "no-inferrable-types": [true, "ignore-params"], 50 | "no-shadowed-variable": true, 51 | "no-string-literal": false, 52 | "no-string-throw": true, 53 | "no-switch-case-fall-through": true, 54 | "no-trailing-whitespace": true, 55 | "no-unused-expression": true, 56 | "no-use-before-declare": true, 57 | "no-var-keyword": true, 58 | "object-literal-sort-keys": false, 59 | "one-line": [ 60 | true, 61 | "check-open-brace", 62 | "check-catch", 63 | "check-else", 64 | "check-whitespace" 65 | ], 66 | "prefer-const": true, 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "radix": true, 72 | "semicolon": [ 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "typeof-compare": true, 90 | "unified-signatures": true, 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | 101 | "directive-selector": [true, "attribute", "app", "camelCase"], 102 | "component-selector": [true, "element", "app", "kebab-case"], 103 | "use-input-property-decorator": true, 104 | "use-output-property-decorator": true, 105 | "use-host-property-decorator": true, 106 | "no-input-rename": true, 107 | "no-output-rename": true, 108 | "use-life-cycle-interface": true, 109 | "use-pipe-transform-interface": true, 110 | "component-class-suffix": true, 111 | "directive-class-suffix": true, 112 | "no-access-missing-member": true, 113 | "templates-use-public": true, 114 | "invoke-injectable": true 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Look in ./config for karma.conf.js 2 | module.exports = require('./config/karma.conf.js'); -------------------------------------------------------------------------------- /ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "dist/", 4 | "lib": { 5 | "entryFile": "public_api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-dnd", 3 | "description": "Angular 2 Drag-and-Drop without dependencies", 4 | "version": "9.0.0-beta2", 5 | "scripts": { 6 | "test": "karma start", 7 | "clean": "rimraf dist", 8 | "test-watch": "tsc --noUnusedParameters --noUnusedLocals && karma start --no-single-run --auto-watch", 9 | "commit": "npm run prepublish && npm test && git-cz", 10 | "build2": "npm run clean && ngc && ng-packagr -p ng-package.json && cp style.css dist/bundles/style.css && rimraf dist/tests", 11 | "build": "ngc && ng-packagr -p ng-package.json && cp style.css dist/bundles/style.css && rimraf dist/tests", 12 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/akserg/ng2-dnd.git" 17 | }, 18 | "keywords": [ 19 | "angular", 20 | "drag", 21 | "drop", 22 | "drag-and-drop" 23 | ], 24 | "author": "Sergey Akopkokhyants ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/akserg/ng2-dnd/issues" 28 | }, 29 | "main": "./bundles/ng2-dnd.umd.js", 30 | "module": "./ng2-dnd.es5.js", 31 | "typings": "./ng2-dnd.d.ts", 32 | "homepage": "https://github.com/akserg/ng2-dnd#readme", 33 | "peerDependencies": { 34 | "@angular/core": "^4.0.0 || ^5.0.0 || ^9.0.0", 35 | "@angular/forms": "^4.0.0 || ^5.0.0 || ^9.0.0" 36 | }, 37 | "dependencies": { 38 | "tslib": "^1.10.0" 39 | }, 40 | "devDependencies": { 41 | "@angular/common": "^9.0.0", 42 | "@angular/compiler": "^9.0.0", 43 | "@angular/compiler-cli": "^9.0.0", 44 | "@angular/core": "^9.0.0", 45 | "@angular/forms": "^9.0.0", 46 | "@angular/platform-browser": "^9.0.0", 47 | "@angular/platform-browser-dynamic": "^9.0.0", 48 | "@angular/platform-server": "^9.0.0", 49 | "@types/hammerjs": "^2.0.36", 50 | "@types/jasmine": "^3.5.0", 51 | "@types/node": "^12.0.53", 52 | "awesome-typescript-loader": "^5.0.0", 53 | "codelyzer": "^6.0.0", 54 | "core-js": "^3.6.0", 55 | "istanbul-instrumenter-loader": "^3.0.0", 56 | "jasmine-core": "^3.6.0", 57 | "karma": "^5.1.0", 58 | "karma-chrome-launcher": "^3.1.0", 59 | "karma-coverage": "^2.0.0", 60 | "karma-firefox-launcher": "^1.3.0", 61 | "karma-jasmine": "^4.0.0", 62 | "karma-mocha-reporter": "^2.2.3", 63 | "karma-remap-coverage": "~0.1.4", 64 | "karma-sourcemap-loader": "^0.3.7", 65 | "karma-webpack": "^4.0.0", 66 | "loader-utils": "^2.0.0", 67 | "ng-packagr": "^10.0.0", 68 | "reflect-metadata": "^0.1.10", 69 | "rxjs": "^6.5.0", 70 | "source-map-loader": "^1.0.0", 71 | "ts-helpers": "1.1.2", 72 | "tslint": "^6.1.0", 73 | "tslint-loader": "^3.5.3", 74 | "typescript": "3.8.3", 75 | "webpack": "^4.0.0", 76 | "zone.js": "^0.10.0" 77 | }, 78 | "engines": { 79 | "npm": ">6.0.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /public_api.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-2020 Sergey Akopkokhyants 2 | // This project is licensed under the terms of the MIT license. 3 | // https://github.com/akserg/ng2-dnd 4 | 5 | export * from './src/dnd.module'; -------------------------------------------------------------------------------- /src/abstract.component.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-2020 Sergey Akopkokhyants 2 | // This project is licensed under the terms of the MIT license. 3 | // https://github.com/akserg/ng2-dnd 4 | 5 | import {Injectable, ChangeDetectorRef, ViewRef} from '@angular/core'; 6 | import {ElementRef} from '@angular/core'; 7 | 8 | import { DragDropConfig, DragImage } from './dnd.config'; 9 | import { DragDropService } from './dnd.service'; 10 | import { isString, isFunction, isPresent, createImage, callFun } from './dnd.utils'; 11 | 12 | @Injectable() 13 | export abstract class AbstractComponent { 14 | _elem: HTMLElement; 15 | _dragHandle: HTMLElement; 16 | _dragHelper: HTMLElement; 17 | _defaultCursor: string; 18 | 19 | /** 20 | * Last element that was mousedown'ed 21 | */ 22 | _target: EventTarget; 23 | 24 | /** 25 | * Whether the object is draggable. Default is true. 26 | */ 27 | private _dragEnabled: boolean = false; 28 | set dragEnabled(enabled: boolean) { 29 | this._dragEnabled = !!enabled; 30 | this._elem.draggable = this._dragEnabled; 31 | } 32 | get dragEnabled(): boolean { 33 | return this._dragEnabled; 34 | } 35 | 36 | /** 37 | * Allows drop on this element 38 | */ 39 | dropEnabled: boolean = false; 40 | /** 41 | * Drag effect 42 | */ 43 | effectAllowed: string; 44 | /** 45 | * Drag cursor 46 | */ 47 | effectCursor: string; 48 | 49 | /** 50 | * Restrict places where a draggable element can be dropped. Either one of 51 | * these two mechanisms can be used: 52 | * 53 | * - dropZones: an array of strings that permits to specify the drop zones 54 | * associated with this component. By default, if the drop-zones attribute 55 | * is not specified, the droppable component accepts drop operations by 56 | * all the draggable components that do not specify the allowed-drop-zones 57 | * 58 | * - allowDrop: a boolean function for droppable components, that is checked 59 | * when an item is dragged. The function is passed the dragData of this 60 | * item. 61 | * - if it returns true, the item can be dropped in this component 62 | * - if it returns false, the item cannot be dropped here 63 | */ 64 | allowDrop: (dropData: any) => boolean; 65 | dropZones: string[] = []; 66 | 67 | /** 68 | * Here is the property dragImage you can use: 69 | * - The string value as url to the image 70 | *
73 | * ... 74 | * - The DragImage value with Image and optional offset by x and y: 75 | * let myDragImage: DragImage = new DragImage("/images/simpler1.png", 0, 0); 76 | * ... 77 | *
80 | * ... 81 | * - The custom function to return the value of dragImage programmatically: 82 | *
85 | * ... 86 | * getDragImage(value:any): string { 87 | * return value ? "/images/simpler1.png" : "/images/simpler2.png" 88 | * } 89 | */ 90 | dragImage: string | DragImage | Function; 91 | 92 | cloneItem: boolean = false; 93 | 94 | constructor(elemRef: ElementRef, public _dragDropService: DragDropService, public _config: DragDropConfig, 95 | private _cdr: ChangeDetectorRef) { 96 | 97 | // Assign default cursor unless overridden 98 | this._defaultCursor = _config.defaultCursor; 99 | this._elem = elemRef.nativeElement; 100 | this._elem.style.cursor = this._defaultCursor; // set default cursor on our element 101 | // 102 | // DROP events 103 | // 104 | this._elem.ondragenter = (event: Event) => { 105 | this._onDragEnter(event); 106 | }; 107 | this._elem.ondragover = (event: DragEvent) => { 108 | this._onDragOver(event); 109 | // 110 | if (event.dataTransfer != null) { 111 | event.dataTransfer.dropEffect = this._config.dropEffect.name; 112 | } 113 | 114 | return false; 115 | }; 116 | this._elem.ondragleave = (event: Event) => { 117 | this._onDragLeave(event); 118 | }; 119 | this._elem.ondrop = (event: Event) => { 120 | this._onDrop(event); 121 | }; 122 | // 123 | // Drag events 124 | // 125 | this._elem.onmousedown = (event: MouseEvent) => { 126 | this._target = event.target; 127 | }; 128 | this._elem.ondragstart = (event: DragEvent) => { 129 | if (this._dragHandle) { 130 | if (!this._dragHandle.contains(this._target)) { 131 | event.preventDefault(); 132 | return; 133 | } 134 | } 135 | 136 | this._onDragStart(event); 137 | // 138 | if (event.dataTransfer != null) { 139 | event.dataTransfer.setData('text', ''); 140 | // Change drag effect 141 | event.dataTransfer.effectAllowed = this.effectAllowed || this._config.dragEffect.name; 142 | // Change drag image 143 | if (isPresent(this.dragImage)) { 144 | if (isString(this.dragImage)) { 145 | (event.dataTransfer).setDragImage(createImage(this.dragImage)); 146 | } else if (isFunction(this.dragImage)) { 147 | (event.dataTransfer).setDragImage(callFun(this.dragImage)); 148 | } else { 149 | let img: DragImage = this.dragImage; 150 | (event.dataTransfer).setDragImage(img.imageElement, img.x_offset, img.y_offset); 151 | } 152 | } else if (isPresent(this._config.dragImage)) { 153 | let dragImage: DragImage = this._config.dragImage; 154 | (event.dataTransfer).setDragImage(dragImage.imageElement, dragImage.x_offset, dragImage.y_offset); 155 | } else if (this.cloneItem) { 156 | this._dragHelper = this._elem.cloneNode(true); 157 | this._dragHelper.classList.add('dnd-drag-item'); 158 | this._dragHelper.style.position = "absolute"; 159 | this._dragHelper.style.top = "0px"; 160 | this._dragHelper.style.left = "-1000px"; 161 | this._elem.parentElement.appendChild(this._dragHelper); 162 | (event.dataTransfer).setDragImage(this._dragHelper, event.offsetX, event.offsetY); 163 | } 164 | 165 | // Change drag cursor 166 | let cursorelem = (this._dragHandle) ? this._dragHandle : this._elem; 167 | 168 | if (this._dragEnabled) { 169 | cursorelem.style.cursor = this.effectCursor ? this.effectCursor : this._config.dragCursor; 170 | } else { 171 | cursorelem.style.cursor = this._defaultCursor; 172 | } 173 | } 174 | }; 175 | 176 | this._elem.ondragend = (event: Event) => { 177 | if (this._elem.parentElement && this._dragHelper) { 178 | this._elem.parentElement.removeChild(this._dragHelper); 179 | } 180 | // console.log('ondragend', event.target); 181 | this._onDragEnd(event); 182 | // Restore style of dragged element 183 | let cursorelem = (this._dragHandle) ? this._dragHandle : this._elem; 184 | cursorelem.style.cursor = this._defaultCursor; 185 | }; 186 | } 187 | 188 | public setDragHandle(elem: HTMLElement) { 189 | this._dragHandle = elem; 190 | } 191 | /******* Change detection ******/ 192 | 193 | detectChanges () { 194 | // Programmatically run change detection to fix issue in Safari 195 | setTimeout(() => { 196 | if ( this._cdr && !(this._cdr as ViewRef).destroyed ) { 197 | this._cdr.detectChanges(); 198 | } 199 | }, 250); 200 | } 201 | 202 | //****** Droppable *******// 203 | private _onDragEnter(event: Event): void { 204 | // console.log('ondragenter._isDropAllowed', this._isDropAllowed); 205 | if (this._isDropAllowed(event)) { 206 | // event.preventDefault(); 207 | this._onDragEnterCallback(event); 208 | } 209 | } 210 | 211 | private _onDragOver(event: Event) { 212 | // // console.log('ondragover._isDropAllowed', this._isDropAllowed); 213 | if (this._isDropAllowed(event)) { 214 | // The element is over the same source element - do nothing 215 | if (event.preventDefault) { 216 | // Necessary. Allows us to drop. 217 | event.preventDefault(); 218 | } 219 | 220 | this._onDragOverCallback(event); 221 | } 222 | } 223 | 224 | private _onDragLeave(event: Event): void { 225 | // console.log('ondragleave._isDropAllowed', this._isDropAllowed); 226 | if (this._isDropAllowed(event)) { 227 | // event.preventDefault(); 228 | this._onDragLeaveCallback(event); 229 | } 230 | } 231 | 232 | private _onDrop(event: Event): void { 233 | // console.log('ondrop._isDropAllowed', this._isDropAllowed); 234 | if (this._isDropAllowed(event)) { 235 | // Necessary. Allows us to drop. 236 | this._preventAndStop(event); 237 | 238 | this._onDropCallback(event); 239 | 240 | this.detectChanges(); 241 | } 242 | } 243 | 244 | private _isDropAllowed(event: any): boolean { 245 | if ((this._dragDropService.isDragged || (event.dataTransfer && event.dataTransfer.files)) && this.dropEnabled) { 246 | // First, if `allowDrop` is set, call it to determine whether the 247 | // dragged element can be dropped here. 248 | if (this.allowDrop) { 249 | return this.allowDrop(this._dragDropService.dragData); 250 | } 251 | 252 | // Otherwise, use dropZones if they are set. 253 | if (this.dropZones.length === 0 && this._dragDropService.allowedDropZones.length === 0) { 254 | return true; 255 | } 256 | for (let i: number = 0; i < this._dragDropService.allowedDropZones.length; i++) { 257 | let dragZone: string = this._dragDropService.allowedDropZones[i]; 258 | if (this.dropZones.indexOf(dragZone) !== -1) { 259 | return true; 260 | } 261 | } 262 | } 263 | return false; 264 | } 265 | 266 | private _preventAndStop(event: Event): any { 267 | if (event.preventDefault) { 268 | event.preventDefault(); 269 | } 270 | if (event.stopPropagation) { 271 | event.stopPropagation(); 272 | } 273 | } 274 | 275 | //*********** Draggable **********// 276 | 277 | private _onDragStart(event: Event): void { 278 | //console.log('ondragstart.dragEnabled', this._dragEnabled); 279 | if (this._dragEnabled) { 280 | this._dragDropService.allowedDropZones = this.dropZones; 281 | // console.log('ondragstart.allowedDropZones', this._dragDropService.allowedDropZones); 282 | this._onDragStartCallback(event); 283 | } 284 | } 285 | 286 | private _onDragEnd(event: Event): void { 287 | this._dragDropService.allowedDropZones = []; 288 | // console.log('ondragend.allowedDropZones', this._dragDropService.allowedDropZones); 289 | this._onDragEndCallback(event); 290 | } 291 | 292 | //**** Drop Callbacks ****// 293 | _onDragEnterCallback(event: Event) { } 294 | _onDragOverCallback(event: Event) { } 295 | _onDragLeaveCallback(event: Event) { } 296 | _onDropCallback(event: Event) { } 297 | 298 | //**** Drag Callbacks ****// 299 | _onDragStartCallback(event: Event) { } 300 | _onDragEndCallback(event: Event) { } 301 | } 302 | 303 | export class AbstractHandleComponent { 304 | _elem: HTMLElement; 305 | constructor(elemRef: ElementRef, public _dragDropService: DragDropService, public _config: DragDropConfig, 306 | private _Component: AbstractComponent, private _cdr: ChangeDetectorRef) { 307 | this._elem = elemRef.nativeElement; 308 | this._Component.setDragHandle(this._elem); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/dnd.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-2020 Sergey Akopkokhyants 2 | // This project is licensed under the terms of the MIT license. 3 | // https://github.com/akserg/ng2-dnd 4 | 5 | import {isString} from './dnd.utils'; 6 | 7 | export class DataTransferEffect { 8 | 9 | static COPY = new DataTransferEffect('copy'); 10 | static LINK = new DataTransferEffect('link'); 11 | static MOVE = new DataTransferEffect('move'); 12 | static NONE = new DataTransferEffect('none'); 13 | 14 | constructor(public name: string) { } 15 | } 16 | 17 | export class DragImage { 18 | constructor( 19 | public imageElement: any, 20 | public x_offset: number = 0, 21 | public y_offset: number = 0) { 22 | if (isString(this.imageElement)) { 23 | // Create real image from string source 24 | let imgScr: string = this.imageElement; 25 | this.imageElement = new HTMLImageElement(); 26 | (this.imageElement).src = imgScr; 27 | } 28 | } 29 | } 30 | 31 | export class DragDropConfig { 32 | public onDragStartClass: string = "dnd-drag-start"; 33 | public onDragEnterClass: string = "dnd-drag-enter"; 34 | public onDragOverClass: string = "dnd-drag-over"; 35 | public onSortableDragClass: string = "dnd-sortable-drag"; 36 | 37 | public dragEffect: DataTransferEffect = DataTransferEffect.MOVE; 38 | public dropEffect: DataTransferEffect = DataTransferEffect.MOVE; 39 | public dragCursor: string = "move"; 40 | public dragImage: DragImage; 41 | public defaultCursor: string = "pointer"; 42 | } -------------------------------------------------------------------------------- /src/dnd.module.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-2020 Sergey Akopkokhyants 2 | // This project is licensed under the terms of the MIT license. 3 | // https://github.com/akserg/ng2-dnd 4 | 5 | import { NgModule, ModuleWithProviders } from "@angular/core"; 6 | 7 | import {DragDropConfig} from './dnd.config'; 8 | import {DragDropService, DragDropSortableService, dragDropServiceFactory, dragDropSortableServiceFactory} from './dnd.service'; 9 | import {DraggableComponent, DraggableHandleComponent} from './draggable.component'; 10 | import {DroppableComponent} from './droppable.component'; 11 | import {SortableContainer, SortableComponent, SortableHandleComponent} from './sortable.component'; 12 | 13 | export * from './abstract.component'; 14 | export * from './dnd.config'; 15 | export * from './dnd.service'; 16 | export * from './draggable.component'; 17 | export * from './droppable.component'; 18 | export * from './sortable.component'; 19 | 20 | export let providers = [ 21 | DragDropConfig, 22 | { provide: DragDropService, useFactory: dragDropServiceFactory }, 23 | { provide: DragDropSortableService, useFactory: dragDropSortableServiceFactory, deps: [DragDropConfig] } 24 | ]; 25 | 26 | @NgModule({ 27 | declarations: [DraggableComponent, DraggableHandleComponent, DroppableComponent, SortableContainer, SortableComponent, SortableHandleComponent], 28 | exports : [DraggableComponent, DraggableHandleComponent, DroppableComponent, SortableContainer, SortableComponent, SortableHandleComponent], 29 | 30 | }) 31 | export class DndModule { 32 | static forRoot(): ModuleWithProviders { 33 | return { 34 | ngModule: DndModule, 35 | providers: providers 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/dnd.service.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-2020 Sergey Akopkokhyants 2 | // This project is licensed under the terms of the MIT license. 3 | // https://github.com/akserg/ng2-dnd 4 | 5 | import {Injectable, EventEmitter} from '@angular/core'; 6 | 7 | import {DragDropConfig} from './dnd.config'; 8 | import {isPresent} from './dnd.utils'; 9 | import {SortableContainer} from './sortable.component'; 10 | 11 | export class DragDropData { 12 | dragData: any; 13 | mouseEvent: MouseEvent; 14 | } 15 | 16 | export function dragDropServiceFactory(): DragDropService { 17 | return new DragDropService(); 18 | } 19 | 20 | @Injectable() 21 | export class DragDropService { 22 | allowedDropZones: Array = []; 23 | onDragSuccessCallback: EventEmitter; 24 | dragData: any; 25 | isDragged: boolean; 26 | } 27 | 28 | export function dragDropSortableServiceFactory(config: DragDropConfig): DragDropSortableService { 29 | return new DragDropSortableService(config); 30 | } 31 | 32 | @Injectable() 33 | export class DragDropSortableService { 34 | index: number; 35 | sortableContainer: SortableContainer; 36 | isDragged: boolean; 37 | 38 | private _elem: HTMLElement; 39 | public get elem(): HTMLElement { 40 | return this._elem; 41 | } 42 | 43 | constructor(private _config:DragDropConfig) {} 44 | 45 | markSortable(elem: HTMLElement) { 46 | if (isPresent(this._elem)) { 47 | this._elem.classList.remove(this._config.onSortableDragClass); 48 | } 49 | if (isPresent(elem)) { 50 | this._elem = elem; 51 | this._elem.classList.add(this._config.onSortableDragClass); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/dnd.utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-2020 Sergey Akopkokhyants 2 | // This project is licensed under the terms of the MIT license. 3 | // https://github.com/akserg/ng2-dnd 4 | 5 | /** 6 | * Check and return true if an object is type of string 7 | */ 8 | export function isString(obj:any) { 9 | return typeof obj === "string"; 10 | } 11 | 12 | /** 13 | * Check and return true if an object not undefined or null 14 | */ 15 | export function isPresent(obj: any) { 16 | return obj !== undefined && obj !== null; 17 | } 18 | 19 | /** 20 | * Check and return true if an object is type of Function 21 | */ 22 | export function isFunction(obj: any) { 23 | return typeof obj === "function"; 24 | } 25 | 26 | /** 27 | * Create Image element with specified url string 28 | */ 29 | export function createImage(src: string) { 30 | let img:HTMLImageElement = new HTMLImageElement(); 31 | img.src = src; 32 | return img; 33 | } 34 | 35 | /** 36 | * Call the function 37 | */ 38 | export function callFun(fun: Function) { 39 | return fun(); 40 | } -------------------------------------------------------------------------------- /src/draggable.component.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-2020 Sergey Akopkokhyants 2 | // This project is licensed under the terms of the MIT license. 3 | // https://github.com/akserg/ng2-dnd 4 | 5 | import {ChangeDetectorRef} from '@angular/core'; 6 | import {Directive, Input, Output, EventEmitter, ElementRef} from '@angular/core'; 7 | 8 | import {AbstractComponent, AbstractHandleComponent} from './abstract.component'; 9 | import {DragDropConfig, DragImage} from './dnd.config'; 10 | import {DragDropService, DragDropData} from './dnd.service'; 11 | 12 | @Directive({ selector: '[dnd-draggable]' }) 13 | export class DraggableComponent extends AbstractComponent { 14 | 15 | @Input("dragEnabled") set draggable(value:boolean) { 16 | this.dragEnabled = !!value; 17 | } 18 | 19 | /** 20 | * Callback function called when the drag actions happened. 21 | */ 22 | @Output() onDragStart: EventEmitter = new EventEmitter(); 23 | @Output() onDragEnd: EventEmitter = new EventEmitter(); 24 | 25 | /** 26 | * The data that has to be dragged. It can be any JS object 27 | */ 28 | @Input() dragData: any; 29 | 30 | /** 31 | * Callback function called when the drag action ends with a valid drop action. 32 | * It is activated after the on-drop-success callback 33 | */ 34 | @Output("onDragSuccess") onDragSuccessCallback: EventEmitter = new EventEmitter(); 35 | 36 | @Input("dropZones") set dropzones(value:Array) { 37 | this.dropZones = value; 38 | } 39 | 40 | /** 41 | * Drag allowed effect 42 | */ 43 | @Input("effectAllowed") set effectallowed(value: string) { 44 | this.effectAllowed = value; 45 | } 46 | 47 | /** 48 | * Drag effect cursor 49 | */ 50 | @Input("effectCursor") set effectcursor(value: string) { 51 | this.effectCursor = value; 52 | } 53 | 54 | /** 55 | * Here is the property dragImage you can use: 56 | * - The string value as url to the image 57 | *
60 | * ... 61 | * - The DragImage value with Image and offset by x and y: 62 | * let myDragImage: DragImage = new DragImage("/images/simpler1.png", 0, 0); 63 | * ... 64 | *
67 | * ... 68 | * - The custom function to return the value of dragImage programmatically: 69 | *
72 | * ... 73 | * getDragImage(value:any): string { 74 | * return value ? "/images/simpler1.png" : "/images/simpler2.png" 75 | * } 76 | */ 77 | @Input() dragImage: string | DragImage | Function; 78 | 79 | 80 | @Input() cloneItem: boolean; 81 | 82 | constructor(elemRef: ElementRef, dragDropService: DragDropService, config:DragDropConfig, 83 | cdr:ChangeDetectorRef) { 84 | 85 | super(elemRef, dragDropService, config, cdr); 86 | this._defaultCursor = this._elem.style.cursor; 87 | this.dragEnabled = true; 88 | } 89 | 90 | _onDragStartCallback(event: MouseEvent) { 91 | this._dragDropService.isDragged = true; 92 | this._dragDropService.dragData = this.dragData; 93 | this._dragDropService.onDragSuccessCallback = this.onDragSuccessCallback; 94 | this._elem.classList.add(this._config.onDragStartClass); 95 | // 96 | this.onDragStart.emit({dragData: this.dragData, mouseEvent: event}); 97 | } 98 | 99 | _onDragEndCallback(event: MouseEvent) { 100 | this._dragDropService.isDragged = false; 101 | this._dragDropService.dragData = null; 102 | this._dragDropService.onDragSuccessCallback = null; 103 | this._elem.classList.remove(this._config.onDragStartClass); 104 | // 105 | this.onDragEnd.emit({dragData: this.dragData, mouseEvent: event}); 106 | } 107 | } 108 | 109 | 110 | @Directive({ selector: '[dnd-draggable-handle]' }) 111 | export class DraggableHandleComponent extends AbstractHandleComponent { 112 | constructor(elemRef: ElementRef, dragDropService: DragDropService, config:DragDropConfig, _Component: DraggableComponent, 113 | cdr:ChangeDetectorRef) { 114 | 115 | super(elemRef, dragDropService, config, _Component, cdr); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/droppable.component.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-2020 Sergey Akopkokhyants 2 | // This project is licensed under the terms of the MIT license. 3 | // https://github.com/akserg/ng2-dnd 4 | 5 | import {ChangeDetectorRef} from '@angular/core'; 6 | import {Directive, Input, Output, EventEmitter, ElementRef} from '@angular/core'; 7 | 8 | import {AbstractComponent} from './abstract.component'; 9 | import {DragDropConfig} from './dnd.config'; 10 | import {DragDropService, DragDropData} from './dnd.service'; 11 | 12 | @Directive({ selector: '[dnd-droppable]' }) 13 | export class DroppableComponent extends AbstractComponent { 14 | 15 | @Input("dropEnabled") set droppable(value:boolean) { 16 | this.dropEnabled = !!value; 17 | } 18 | 19 | /** 20 | * Callback function called when the drop action completes correctly. 21 | * It is activated before the on-drag-success callback. 22 | */ 23 | @Output() onDropSuccess: EventEmitter = new EventEmitter(); 24 | @Output() onDragEnter: EventEmitter = new EventEmitter(); 25 | @Output() onDragOver: EventEmitter = new EventEmitter(); 26 | @Output() onDragLeave: EventEmitter = new EventEmitter(); 27 | 28 | @Input("allowDrop") set allowdrop(value: (dropData: any) => boolean) { 29 | this.allowDrop = value; 30 | } 31 | 32 | @Input("dropZones") set dropzones(value:Array) { 33 | this.dropZones = value; 34 | } 35 | 36 | /** 37 | * Drag allowed effect 38 | */ 39 | @Input("effectAllowed") set effectallowed(value: string) { 40 | this.effectAllowed = value; 41 | } 42 | 43 | /** 44 | * Drag effect cursor 45 | */ 46 | @Input("effectCursor") set effectcursor(value: string) { 47 | this.effectCursor = value; 48 | } 49 | 50 | constructor(elemRef: ElementRef, dragDropService: DragDropService, config:DragDropConfig, 51 | cdr:ChangeDetectorRef) { 52 | 53 | super(elemRef, dragDropService, config, cdr); 54 | 55 | this.dropEnabled = true; 56 | } 57 | 58 | _onDragEnterCallback(event: MouseEvent) { 59 | if (this._dragDropService.isDragged) { 60 | this._elem.classList.add(this._config.onDragEnterClass); 61 | this.onDragEnter.emit({dragData: this._dragDropService.dragData, mouseEvent: event}); 62 | } 63 | } 64 | 65 | _onDragOverCallback (event: MouseEvent) { 66 | if (this._dragDropService.isDragged) { 67 | this._elem.classList.add(this._config.onDragOverClass); 68 | this.onDragOver.emit({dragData: this._dragDropService.dragData, mouseEvent: event}); 69 | } 70 | }; 71 | 72 | _onDragLeaveCallback (event: MouseEvent) { 73 | if (this._dragDropService.isDragged) { 74 | this._elem.classList.remove(this._config.onDragOverClass); 75 | this._elem.classList.remove(this._config.onDragEnterClass); 76 | this.onDragLeave.emit({dragData: this._dragDropService.dragData, mouseEvent: event}); 77 | } 78 | }; 79 | 80 | _onDropCallback (event: MouseEvent) { 81 | let dataTransfer = (event as any).dataTransfer; 82 | if (this._dragDropService.isDragged || (dataTransfer && dataTransfer.files)) { 83 | this.onDropSuccess.emit({dragData: this._dragDropService.dragData, mouseEvent: event}); 84 | if (this._dragDropService.onDragSuccessCallback) { 85 | this._dragDropService.onDragSuccessCallback.emit({dragData: this._dragDropService.dragData, mouseEvent: event}); 86 | } 87 | this._elem.classList.remove(this._config.onDragOverClass); 88 | this._elem.classList.remove(this._config.onDragEnterClass); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/sortable.component.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016-2020 Sergey Akopkokhyants 2 | // This project is licensed under the terms of the MIT license. 3 | // https://github.com/akserg/ng2-dnd 4 | 5 | import {ChangeDetectorRef} from '@angular/core'; 6 | import {Directive, Input, Output, EventEmitter, ElementRef} from '@angular/core'; 7 | import {FormArray} from '@angular/forms'; 8 | 9 | import {AbstractComponent, AbstractHandleComponent} from './abstract.component'; 10 | import {DragDropConfig} from './dnd.config'; 11 | import {DragDropService, DragDropSortableService} from './dnd.service'; 12 | 13 | @Directive({ selector: '[dnd-sortable-container]' }) 14 | export class SortableContainer extends AbstractComponent { 15 | 16 | @Input("dragEnabled") set draggable(value:boolean) { 17 | this.dragEnabled = !!value; 18 | } 19 | 20 | private _sortableData: Array|FormArray = []; 21 | private sortableHandler: SortableFormArrayHandler|SortableArrayHandler; 22 | 23 | @Input() set sortableData(sortableData: Array|FormArray) { 24 | this._sortableData = sortableData; 25 | if (sortableData instanceof FormArray) { 26 | this.sortableHandler = new SortableFormArrayHandler(); 27 | } else { 28 | this.sortableHandler = new SortableArrayHandler(); 29 | } 30 | // 31 | this.dropEnabled = !!this._sortableData; 32 | // console.log("collection is changed, drop enabled: " + this.dropEnabled); 33 | } 34 | get sortableData(): Array|FormArray { 35 | return this._sortableData; 36 | } 37 | 38 | @Input("dropZones") set dropzones(value:Array) { 39 | this.dropZones = value; 40 | } 41 | 42 | constructor(elemRef: ElementRef, dragDropService: DragDropService, config:DragDropConfig, cdr:ChangeDetectorRef, 43 | private _sortableDataService: DragDropSortableService) { 44 | 45 | super(elemRef, dragDropService, config, cdr); 46 | this.dragEnabled = false; 47 | } 48 | 49 | _onDragEnterCallback(event: Event) { 50 | if (this._sortableDataService.isDragged) { 51 | let item:any = this._sortableDataService.sortableContainer.getItemAt(this._sortableDataService.index); 52 | // Check does element exist in sortableData of this Container 53 | if (this.indexOf(item) === -1) { 54 | // Let's add it 55 | // console.log('Container._onDragEnterCallback. drag node [' + this._sortableDataService.index.toString() + '] over parent node'); 56 | // Remove item from previouse list 57 | this._sortableDataService.sortableContainer.removeItemAt(this._sortableDataService.index); 58 | if (this._sortableDataService.sortableContainer._sortableData.length === 0) { 59 | this._sortableDataService.sortableContainer.dropEnabled = true; 60 | } 61 | // Add item to new list 62 | this.insertItemAt(item, 0); 63 | this._sortableDataService.sortableContainer = this; 64 | this._sortableDataService.index = 0; 65 | } 66 | // Refresh changes in properties of container component 67 | this.detectChanges(); 68 | } 69 | } 70 | 71 | getItemAt(index: number): any { 72 | return this.sortableHandler.getItemAt(this._sortableData, index); 73 | } 74 | 75 | indexOf(item: any): number { 76 | return this.sortableHandler.indexOf(this._sortableData, item); 77 | } 78 | 79 | removeItemAt(index: number): void { 80 | this.sortableHandler.removeItemAt(this._sortableData, index); 81 | } 82 | 83 | insertItemAt(item: any, index: number) { 84 | this.sortableHandler.insertItemAt(this._sortableData, item, index); 85 | } 86 | } 87 | 88 | class SortableArrayHandler { 89 | getItemAt(sortableData: any, index: number): any { 90 | return sortableData[index]; 91 | } 92 | 93 | indexOf(sortableData: any, item: any): number { 94 | return sortableData.indexOf(item); 95 | } 96 | 97 | removeItemAt(sortableData: any, index: number) { 98 | sortableData.splice(index, 1); 99 | } 100 | 101 | insertItemAt(sortableData: any, item: any, index: number) { 102 | sortableData.splice(index, 0, item); 103 | } 104 | } 105 | 106 | class SortableFormArrayHandler { 107 | getItemAt(sortableData: any, index: number): any { 108 | return sortableData.at(index); 109 | } 110 | 111 | indexOf(sortableData: any, item: any): number { 112 | return sortableData.controls.indexOf(item); 113 | } 114 | 115 | removeItemAt(sortableData: any, index: number) { 116 | sortableData.removeAt(index); 117 | } 118 | 119 | insertItemAt(sortableData: any, item: any, index: number) { 120 | sortableData.insert(index, item); 121 | } 122 | } 123 | 124 | @Directive({ selector: '[dnd-sortable]' }) 125 | export class SortableComponent extends AbstractComponent { 126 | 127 | @Input('sortableIndex') index: number; 128 | 129 | @Input("dragEnabled") set draggable(value:boolean) { 130 | this.dragEnabled = !!value; 131 | } 132 | 133 | @Input("dropEnabled") set droppable(value:boolean) { 134 | this.dropEnabled = !!value; 135 | } 136 | 137 | /** 138 | * The data that has to be dragged. It can be any JS object 139 | */ 140 | @Input() dragData: any; 141 | 142 | /** 143 | * Drag allowed effect 144 | */ 145 | @Input("effectAllowed") set effectallowed(value: string) { 146 | this.effectAllowed = value; 147 | } 148 | 149 | /** 150 | * Drag effect cursor 151 | */ 152 | @Input("effectCursor") set effectcursor(value: string) { 153 | this.effectCursor = value; 154 | } 155 | 156 | /** 157 | * Callback function called when the drag action ends with a valid drop action. 158 | * It is activated after the on-drop-success callback 159 | */ 160 | @Output("onDragSuccess") onDragSuccessCallback: EventEmitter = new EventEmitter(); 161 | 162 | @Output("onDragStart") onDragStartCallback: EventEmitter = new EventEmitter(); 163 | @Output("onDragOver") onDragOverCallback: EventEmitter = new EventEmitter(); 164 | @Output("onDragEnd") onDragEndCallback: EventEmitter = new EventEmitter(); 165 | @Output("onDropSuccess") onDropSuccessCallback: EventEmitter = new EventEmitter(); 166 | 167 | constructor(elemRef: ElementRef, dragDropService: DragDropService, config:DragDropConfig, 168 | private _sortableContainer: SortableContainer, 169 | private _sortableDataService: DragDropSortableService, 170 | cdr:ChangeDetectorRef) { 171 | super(elemRef, dragDropService, config, cdr); 172 | this.dropZones = this._sortableContainer.dropZones; 173 | this.dragEnabled = true; 174 | this.dropEnabled = true; 175 | } 176 | 177 | _onDragStartCallback(event: Event) { 178 | // console.log('_onDragStartCallback. dragging elem with index ' + this.index); 179 | this._sortableDataService.isDragged = true; 180 | this._sortableDataService.sortableContainer = this._sortableContainer; 181 | this._sortableDataService.index = this.index; 182 | this._sortableDataService.markSortable(this._elem); 183 | // Add dragData 184 | this._dragDropService.isDragged = true; 185 | this._dragDropService.dragData = this.dragData; 186 | this._dragDropService.onDragSuccessCallback = this.onDragSuccessCallback; 187 | // 188 | this.onDragStartCallback.emit(this._dragDropService.dragData); 189 | } 190 | 191 | _onDragOverCallback(event: Event) { 192 | if (this._sortableDataService.isDragged && this._elem !== this._sortableDataService.elem) { 193 | // console.log('_onDragOverCallback. dragging elem with index ' + this.index); 194 | this._sortableDataService.sortableContainer = this._sortableContainer; 195 | this._sortableDataService.index = this.index; 196 | this._sortableDataService.markSortable(this._elem); 197 | this.onDragOverCallback.emit(this._dragDropService.dragData); 198 | } 199 | } 200 | 201 | _onDragEndCallback(event: Event) { 202 | // console.log('_onDragEndCallback. end dragging elem with index ' + this.index); 203 | this._sortableDataService.isDragged = false; 204 | this._sortableDataService.sortableContainer = null; 205 | this._sortableDataService.index = null; 206 | this._sortableDataService.markSortable(null); 207 | // Add dragGata 208 | this._dragDropService.isDragged = false; 209 | this._dragDropService.dragData = null; 210 | this._dragDropService.onDragSuccessCallback = null; 211 | // 212 | this.onDragEndCallback.emit(this._dragDropService.dragData); 213 | } 214 | 215 | _onDragEnterCallback(event: Event) { 216 | if (this._sortableDataService.isDragged) { 217 | this._sortableDataService.markSortable(this._elem); 218 | if ((this.index !== this._sortableDataService.index) || 219 | (this._sortableDataService.sortableContainer.sortableData !== this._sortableContainer.sortableData)) { 220 | // console.log('Component._onDragEnterCallback. drag node [' + this.index + '] over node [' + this._sortableDataService.index + ']'); 221 | // Get item 222 | let item:any = this._sortableDataService.sortableContainer.getItemAt(this._sortableDataService.index); 223 | // Remove item from previouse list 224 | this._sortableDataService.sortableContainer.removeItemAt(this._sortableDataService.index); 225 | if (this._sortableDataService.sortableContainer.sortableData.length === 0) { 226 | this._sortableDataService.sortableContainer.dropEnabled = true; 227 | } 228 | // Add item to new list 229 | this._sortableContainer.insertItemAt(item, this.index); 230 | if (this._sortableContainer.dropEnabled) { 231 | this._sortableContainer.dropEnabled = false; 232 | } 233 | this._sortableDataService.sortableContainer = this._sortableContainer; 234 | this._sortableDataService.index = this.index; 235 | this.detectChanges(); 236 | } 237 | } 238 | } 239 | 240 | _onDropCallback (event: Event) { 241 | if (this._sortableDataService.isDragged) { 242 | // console.log('onDropCallback.onDropSuccessCallback.dragData', this._dragDropService.dragData); 243 | this.onDropSuccessCallback.emit(this._dragDropService.dragData); 244 | if (this._dragDropService.onDragSuccessCallback) { 245 | // console.log('onDropCallback.onDragSuccessCallback.dragData', this._dragDropService.dragData); 246 | this._dragDropService.onDragSuccessCallback.emit(this._dragDropService.dragData); 247 | } 248 | // Refresh changes in properties of container component 249 | this._sortableContainer.detectChanges(); 250 | } 251 | } 252 | } 253 | 254 | @Directive({ selector: '[dnd-sortable-handle]' }) 255 | export class SortableHandleComponent extends AbstractHandleComponent { 256 | constructor(elemRef: ElementRef, dragDropService: DragDropService, config:DragDropConfig, _Component: SortableComponent, 257 | cdr:ChangeDetectorRef) { 258 | 259 | super(elemRef, dragDropService, config, _Component, cdr); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | .dnd-drag-start { 2 | -moz-transform:scale(0.8); 3 | -webkit-transform:scale(0.8); 4 | transform:scale(0.8); 5 | opacity:0.7; 6 | border: 2px dashed #000; 7 | } 8 | 9 | .dnd-drag-enter { 10 | opacity:0.7; 11 | border: 2px dashed #000; 12 | } 13 | 14 | .dnd-drag-over { 15 | border: 2px dashed #000; 16 | } 17 | 18 | .dnd-sortable-drag { 19 | -moz-transform:scale(0.9); 20 | -webkit-transform:scale(0.9); 21 | transform:scale(0.9); 22 | opacity:0.7; 23 | border: 1px dashed #000; 24 | } 25 | -------------------------------------------------------------------------------- /tests/dnd.component.factory.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, Output, EventEmitter} from '@angular/core'; 2 | 3 | export function triggerEvent(elem:HTMLElement, eventName:string, eventType:string) { 4 | var event:Event = document.createEvent(eventType); 5 | event.initEvent(eventName, true, true); 6 | elem.dispatchEvent(event); 7 | } 8 | 9 | @Component({ 10 | selector: 'test-container', 11 | template: ` 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | ` 20 | }) 21 | export class Container { 22 | @Output() dragOne:EventEmitter = new EventEmitter(); 23 | @Output() dragTwo:EventEmitter = new EventEmitter(); 24 | @Output() dragOneTwo:EventEmitter = new EventEmitter(); 25 | 26 | @Output() dropOne:EventEmitter = new EventEmitter(); 27 | @Output() dropTwo:EventEmitter = new EventEmitter(); 28 | @Output() dropOneTwo:EventEmitter = new EventEmitter(); 29 | 30 | // tslint:disable-next-line 31 | private dragOneSuccessCallback($event:any) { 32 | this.dragOne.emit($event); 33 | } 34 | 35 | // tslint:disable-next-line 36 | private dragTwoSuccessCallback($event:any) { 37 | this.dragOne.emit($event); 38 | } 39 | 40 | // tslint:disable-next-line 41 | private dragOneTwoSuccessCallback($event:any) { 42 | this.dragOneTwo.emit($event); 43 | } 44 | 45 | // tslint:disable-next-line 46 | private dropOneSuccessCallback($event:any) { 47 | this.dropOne.emit($event); 48 | } 49 | 50 | // tslint:disable-next-line 51 | private dropTwoSuccessCallback($event:any) { 52 | this.dropTwo.emit($event); 53 | } 54 | 55 | // tslint:disable-next-line 56 | private dropOneTwoSuccessCallback($event:any) { 57 | this.dropOneTwo.emit($event); 58 | } 59 | } 60 | 61 | @Component({ 62 | selector: 'test-container-two', 63 | template: ` 64 |
65 |
66 | ` 67 | }) 68 | export class Container2 { 69 | @Input() dragEnabled:boolean = true; 70 | @Input() dragData:any = "Hello World at " + new Date().toString(); 71 | 72 | @Output() drag:EventEmitter = new EventEmitter(); 73 | @Output() drop:EventEmitter = new EventEmitter(); 74 | 75 | // tslint:disable-next-line 76 | private dragSuccessCallback($event:any) { 77 | this.drag.emit($event); 78 | } 79 | 80 | // tslint:disable-next-line 81 | private dropSuccessCallback($event:any) { 82 | this.drop.emit($event); 83 | } 84 | } 85 | 86 | @Component({ 87 | selector: 'test-container-three', 88 | template: ` 89 |
90 |
    91 |
  • {{item}}
  • 92 |
93 |
94 | ` 95 | }) 96 | export class Container3 { 97 | @Input() sortableList:Array = []; 98 | } 99 | 100 | @Component({ 101 | selector: 'test-container-four', 102 | template: ` 103 |
104 |
105 |
    106 |
  • {{item}}
  • 107 |
108 |
109 |
110 |
    111 |
  • {{item}}
  • 112 |
113 |
114 |
115 |
    116 |
  • {{item}}
  • 117 |
118 |
119 |
120 | ` 121 | }) 122 | export class Container4 { 123 | @Input() singleList:Array = []; 124 | @Input() multiOneList:Array = []; 125 | @Input() multiTwoList:Array = []; 126 | } 127 | 128 | @Component({ 129 | selector: 'test-container-five', 130 | template: ` 131 |
132 | = 133 | Not handle 134 |
135 |
136 | ` 137 | }) 138 | export class Container5 { 139 | @Input() dragEnabled:boolean = true; 140 | @Input() dragData:any = "Hello World at " + new Date().toString(); 141 | 142 | @Output() drag:EventEmitter = new EventEmitter(); 143 | @Output() drop:EventEmitter = new EventEmitter(); 144 | 145 | // tslint:disable-next-line 146 | private dragSuccessCallback($event:any) { 147 | this.drag.emit($event); 148 | } 149 | 150 | // tslint:disable-next-line 151 | private dropSuccessCallback($event:any) { 152 | this.drop.emit($event); 153 | } 154 | } 155 | 156 | @Component({ 157 | selector: 'test-container-six', 158 | template: ` 159 |
160 |
    161 |
  • 162 | = 163 | {{item}} 164 |
  • 165 |
166 |
167 | ` 168 | }) 169 | export class Container6 { 170 | @Input() sortableList:Array = []; 171 | } -------------------------------------------------------------------------------- /tests/dnd.draggable.handle.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed, ComponentFixture } 2 | from '@angular/core/testing'; 3 | 4 | import {DragDropConfig} from '../src/dnd.config'; 5 | import {DraggableComponent, DraggableHandleComponent} from '../src/draggable.component'; 6 | import {DroppableComponent} from '../src/droppable.component'; 7 | import {DragDropService} from '../src/dnd.service'; 8 | 9 | import {Container5, triggerEvent} from './dnd.component.factory'; 10 | 11 | describe('Drag and Drop with handle', () => { 12 | 13 | let componentFixture: ComponentFixture; 14 | let dragdropService: DragDropService; 15 | let config: DragDropConfig; 16 | let container:Container5; 17 | 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [DraggableComponent, DroppableComponent, DraggableHandleComponent, Container5], 21 | providers: [DragDropConfig, DragDropService] 22 | }); 23 | TestBed.compileComponents(); 24 | }); 25 | 26 | beforeEach(inject([DragDropConfig, DragDropService], 27 | (c: DragDropConfig, dd: DragDropService) => { 28 | dragdropService = dd; 29 | config = c; 30 | 31 | componentFixture = TestBed.createComponent(Container5); 32 | componentFixture.detectChanges(); 33 | container = componentFixture.componentInstance; 34 | })); 35 | 36 | it('should be defined', () => { 37 | expect(componentFixture).toBeDefined(); 38 | }); 39 | 40 | it('Drag start event should be activated if dragged by handle', (done:any) => { 41 | let dragElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dragId'); 42 | let handleElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#handle'); 43 | 44 | expect(dragdropService.dragData).not.toBeDefined(); 45 | 46 | triggerEvent(handleElem, 'mousedown', 'MouseEvent'); 47 | triggerEvent(dragElem, 'dragstart', 'MouseEvent'); 48 | componentFixture.detectChanges(); 49 | expect(dragdropService.dragData).toBeDefined(); 50 | 51 | triggerEvent(dragElem, 'dragend', 'MouseEvent'); 52 | triggerEvent(handleElem, 'mouseup', 'MouseEvent'); 53 | componentFixture.detectChanges(); 54 | expect(dragdropService.dragData).toBeNull(); 55 | 56 | done(); 57 | }); 58 | 59 | it('Drag start event should not be activated if dragged not by handle', (done:any) => { 60 | container.dragEnabled = false; 61 | componentFixture.detectChanges(); 62 | 63 | let dragElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dragId'); 64 | let nonHandleElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#non-handle'); 65 | 66 | expect(dragdropService.dragData).not.toBeDefined(); 67 | expect(dragElem.classList.contains(config.onDragStartClass)).toEqual(false); 68 | 69 | triggerEvent(nonHandleElem, 'mousedown', 'MouseEvent'); 70 | triggerEvent(dragElem, 'dragstart', 'MouseEvent'); 71 | componentFixture.detectChanges(); 72 | expect(dragdropService.dragData).not.toBeDefined(); 73 | expect(dragElem.classList.contains(config.onDragStartClass)).toEqual(false); 74 | 75 | done(); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tests/dnd.sortable.handle.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed, ComponentFixture } 2 | from '@angular/core/testing'; 3 | 4 | import {DragDropConfig} from '../src/dnd.config'; 5 | import {SortableContainer, SortableComponent, SortableHandleComponent} from '../src/sortable.component'; 6 | import {DragDropService, DragDropSortableService} from '../src/dnd.service'; 7 | 8 | import {Container6, triggerEvent} from './dnd.component.factory'; 9 | 10 | describe('Sortable Drag and Drop with handle', () => { 11 | 12 | let componentFixture: ComponentFixture; 13 | let dragdropService: DragDropService; 14 | let config: DragDropConfig; 15 | let container: Container6; 16 | let sortableService: DragDropSortableService; 17 | 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [SortableContainer, SortableComponent, SortableHandleComponent, Container6], 21 | providers: [DragDropConfig, 22 | DragDropService, 23 | DragDropSortableService] 24 | }); 25 | TestBed.compileComponents(); 26 | }); 27 | 28 | beforeEach(inject([DragDropConfig, DragDropService, DragDropSortableService], 29 | (c: DragDropConfig, dd: DragDropService, ds: DragDropSortableService) => { 30 | dragdropService = dd; 31 | config = c; 32 | sortableService = ds; 33 | 34 | componentFixture = TestBed.createComponent(Container6); 35 | componentFixture.detectChanges(); 36 | container = componentFixture.componentInstance; 37 | })); 38 | 39 | it('should be defined', () => { 40 | expect(componentFixture).toBeDefined(); 41 | }); 42 | 43 | it('The elements of the list should be draggable by handle', () => { 44 | let values:Array = ['one','two','three','four']; 45 | 46 | container.sortableList = values; 47 | componentFixture.detectChanges(); 48 | 49 | let ulElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('ul'); 50 | 51 | expect(ulElem).toBeDefined(); 52 | expect(ulElem.children.length).toBe(values.length); 53 | 54 | expect(sortableService.sortableContainer).not.toBeDefined(); 55 | expect(sortableService.index).not.toBeDefined(); 56 | 57 | triggerEvent(ulElem.children[0].querySelector('.handle'), 'mousedown', 'MouseEvent'); 58 | triggerEvent(ulElem.children[0], 'dragstart', 'MouseEvent'); 59 | componentFixture.detectChanges(); 60 | expect(sortableService.sortableContainer.sortableData).toBe(values); 61 | expect(sortableService.index).toBe(0); 62 | }); 63 | 64 | it('The elements of the list should not be draggable by non-handle', () => { 65 | let values:Array = ['one','two','three','four']; 66 | 67 | container.sortableList = values; 68 | componentFixture.detectChanges(); 69 | 70 | let ulElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('ul'); 71 | 72 | expect(ulElem).toBeDefined(); 73 | expect(ulElem.children.length).toBe(values.length); 74 | 75 | expect(sortableService.sortableContainer).not.toBeDefined(); 76 | expect(sortableService.index).not.toBeDefined(); 77 | 78 | triggerEvent(ulElem.children[0].querySelector('.non-handle'), 'mousedown', 'MouseEvent'); 79 | triggerEvent(ulElem.children[0], 'dragstart', 'MouseEvent'); 80 | componentFixture.detectChanges(); 81 | expect(sortableService.sortableContainer).not.toBeDefined(); 82 | expect(sortableService.index).not.toBeDefined(); 83 | }); 84 | }); 85 | 86 | -------------------------------------------------------------------------------- /tests/dnd.sortable.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed, ComponentFixture } 2 | from '@angular/core/testing'; 3 | 4 | import {DragDropConfig} from '../src/dnd.config'; 5 | import {SortableContainer, SortableComponent} from '../src/sortable.component'; 6 | import {DragDropService, DragDropSortableService} from '../src/dnd.service'; 7 | 8 | import {Container3, Container4, triggerEvent} from './dnd.component.factory'; 9 | 10 | describe('Sortable Drag and Drop', () => { 11 | 12 | let componentFixture: ComponentFixture; 13 | let dragdropService: DragDropService; 14 | let config: DragDropConfig; 15 | let container: Container3; 16 | let sortableService: DragDropSortableService; 17 | 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [SortableContainer, SortableComponent, Container3], 21 | providers: [DragDropConfig, 22 | DragDropService, 23 | DragDropSortableService] 24 | }); 25 | TestBed.compileComponents(); 26 | }); 27 | 28 | beforeEach(inject([DragDropConfig, DragDropService, DragDropSortableService], 29 | (c: DragDropConfig, dd: DragDropService, ds: DragDropSortableService) => { 30 | dragdropService = dd; 31 | config = c; 32 | sortableService = ds; 33 | 34 | componentFixture = TestBed.createComponent(Container3); 35 | componentFixture.detectChanges(); 36 | container = componentFixture.componentInstance; 37 | })); 38 | 39 | it('should be defined', () => { 40 | expect(componentFixture).toBeDefined(); 41 | }); 42 | 43 | it('The elements of the list should be draggable', () => { 44 | let values:Array = ['one', 'two', 'three', 'four', 'five', 'six']; 45 | 46 | container.sortableList = values; 47 | componentFixture.detectChanges(); 48 | 49 | let ulElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('ul'); 50 | expect(ulElem).toBeDefined(); 51 | expect(ulElem.children.length).toBe(values.length); 52 | 53 | for (let i:number = 0; i < ulElem.children.length; i++) { 54 | let childElem:HTMLElement = ulElem.children[i]; 55 | expect(childElem.attributes['draggable']).toBeTruthy(); 56 | } 57 | }); 58 | 59 | it('It should sort in the same list', () => { 60 | let values:Array = ['one','two','three','four']; 61 | 62 | container.sortableList = values; 63 | componentFixture.detectChanges(); 64 | 65 | let ulElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('ul'); 66 | expect(ulElem).toBeDefined(); 67 | expect(ulElem.children.length).toBe(values.length); 68 | 69 | expect(sortableService.sortableContainer).not.toBeDefined(); 70 | expect(sortableService.index).not.toBeDefined(); 71 | 72 | triggerEvent(ulElem.children[0], 'dragstart', 'MouseEvent'); 73 | componentFixture.detectChanges(); 74 | expect(sortableService.sortableContainer.sortableData).toBe(values); 75 | expect(sortableService.index).toBe(0); 76 | 77 | swap(ulElem.children, 0, 1); 78 | componentFixture.detectChanges(); 79 | expect(values[0]).toBe('two'); 80 | expect(ulElem.children[0].textContent).toBe('two'); 81 | expect(values[1]).toBe('one'); 82 | expect(ulElem.children[1].textContent).toBe('one'); 83 | }); 84 | 85 | it('It should work with arbitrary objects', () => { 86 | let elemOne:HTMLDivElement = document.createElement('div'); 87 | let elemTwo = 'elemTwo'; 88 | let elemThree = {'key':'value'}; 89 | let values:Array = [elemOne, elemTwo, elemThree]; 90 | 91 | container.sortableList = values; 92 | componentFixture.detectChanges(); 93 | 94 | let ulElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('ul'); 95 | expect(ulElem).toBeDefined(); 96 | expect(ulElem.children.length).toBe(values.length); 97 | 98 | swap(ulElem.children, 0, 1); 99 | expect(values[0]).toBe(elemTwo); 100 | expect(values[1]).toBe(elemOne); 101 | 102 | swap(ulElem.children, 1, 2); 103 | expect(values[1]).toBe(elemThree); 104 | expect(values[2]).toBe(elemOne); 105 | 106 | swap(ulElem.children, 0, 1); 107 | expect(values[0]).toBe(elemThree); 108 | expect(values[1]).toBe(elemTwo); 109 | }); 110 | }); 111 | 112 | describe('Multi List Sortable Drag and Drop', () => { 113 | 114 | let componentFixture: ComponentFixture; 115 | let dragdropService: DragDropService; 116 | let config: DragDropConfig; 117 | let container:Container4; 118 | let sortableService:DragDropSortableService; 119 | 120 | beforeEach(() => { 121 | TestBed.configureTestingModule({ 122 | declarations: [SortableContainer, SortableComponent, Container4], 123 | providers: [DragDropConfig, DragDropService, DragDropSortableService] 124 | }); 125 | TestBed.compileComponents(); 126 | }); 127 | 128 | beforeEach(inject([DragDropConfig, DragDropService, DragDropSortableService], 129 | (c: DragDropConfig, dd: DragDropService, ds: DragDropSortableService) => { 130 | dragdropService = dd; 131 | config = c; 132 | sortableService = ds; 133 | 134 | componentFixture = TestBed.createComponent(Container4); 135 | componentFixture.detectChanges(); 136 | container = componentFixture.componentInstance; 137 | })); 138 | 139 | it('should be defined', () => { 140 | expect(componentFixture).toBeDefined(); 141 | }); 142 | 143 | it('It should sort in the same list', () => { 144 | let singleList:Array = ['sOne', 'sTwo', 'sThree']; 145 | let multiOneList:Array = ['mOne', 'mTwo', 'mThree']; 146 | let multiTwoList:Array = ['mFour', 'mFive', 'mSix']; 147 | 148 | container.singleList = singleList; 149 | container.multiOneList = multiOneList; 150 | container.multiTwoList = multiTwoList; 151 | componentFixture.detectChanges(); 152 | 153 | let divElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('div'); 154 | expect(divElem).toBeDefined(); 155 | expect(divElem.children.length).toBe(3); 156 | 157 | let singleElem:HTMLElement = divElem.querySelector('#single ul'); 158 | swap(singleElem.children, 0, 1); 159 | componentFixture.detectChanges(); 160 | expect(singleList[0]).toBe('sTwo'); 161 | expect(singleElem.children[0].textContent).toEqual('sTwo'); 162 | expect(singleList[1]).toBe('sOne'); 163 | expect(singleElem.children[1].textContent).toEqual('sOne'); 164 | 165 | let multiOneElem:HTMLElement = divElem.querySelector('#multiOne ul'); 166 | swap(multiOneElem.children, 1, 2); 167 | componentFixture.detectChanges(); 168 | expect(multiOneList[1]).toBe('mThree'); 169 | expect(multiOneElem.children[1].textContent).toEqual('mThree'); 170 | expect(multiOneList[2]).toBe('mTwo'); 171 | expect(multiOneElem.children[2].textContent).toEqual('mTwo'); 172 | 173 | let multiTwoElem:HTMLElement = divElem.querySelector('#multiTwo ul'); 174 | swap(multiTwoElem.children, 1, 2); 175 | componentFixture.detectChanges(); 176 | expect(multiTwoList[1]).toBe('mSix'); 177 | expect(multiTwoElem.children[1].textContent).toEqual('mSix'); 178 | expect(multiTwoList[2]).toBe('mFive'); 179 | expect(multiTwoElem.children[2].textContent).toEqual('mFive'); 180 | }); 181 | 182 | it('It should be possible to move items from list one to list two', () => { 183 | let singleList:Array = ['sOne', 'sTwo', 'sThree']; 184 | let multiOneList:Array = ['mOne', 'mTwo', 'mThree']; 185 | let multiTwoList:Array = ['mFour', 'mFive', 'mSix']; 186 | 187 | container.singleList = singleList; 188 | container.multiOneList = multiOneList; 189 | container.multiTwoList = multiTwoList; 190 | componentFixture.detectChanges(); 191 | 192 | let divElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('div'); 193 | expect(divElem).toBeDefined(); 194 | expect(divElem.children.length).toBe(3); 195 | 196 | let multiOneElem:HTMLElement = divElem.querySelector('#multiOne ul'); 197 | let multiTwoElem:HTMLElement = divElem.querySelector('#multiTwo ul'); 198 | swapMultiple(multiOneElem.children, 0, multiTwoElem.children, 0); 199 | componentFixture.detectChanges(); 200 | 201 | expect(multiOneList.length).toBe(2); 202 | expect(multiTwoList.length).toBe(4); 203 | 204 | expect(multiOneList[0]).toBe('mTwo'); 205 | expect(multiTwoList[0]).toBe('mOne'); 206 | expect(multiTwoList[1]).toBe('mFour'); 207 | }); 208 | 209 | it('It should not be possible to move items between lists not in the same sortable-zone', () => { 210 | let singleList:Array = ['sOne', 'sTwo', 'sThree']; 211 | let multiOneList:Array = ['mOne', 'mTwo', 'mThree']; 212 | let multiTwoList:Array = ['mFour', 'mFive', 'mSix']; 213 | 214 | container.singleList = singleList; 215 | container.multiOneList = multiOneList; 216 | container.multiTwoList = multiTwoList; 217 | componentFixture.detectChanges(); 218 | 219 | let divElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('div'); 220 | expect(divElem).toBeDefined(); 221 | expect(divElem.children.length).toBe(3); 222 | 223 | let singleElem:HTMLElement = divElem.querySelector('#single ul'); 224 | let multiOneElem:HTMLElement = divElem.querySelector('#multiOne ul'); 225 | swapMultiple(singleElem.children, 0, multiOneElem.children, 0); 226 | componentFixture.detectChanges(); 227 | 228 | expect(singleList.length).toBe(3); 229 | expect(multiOneList.length).toBe(3); 230 | 231 | expect(singleList[0]).toBe('sOne'); 232 | expect(multiOneList[0]).toBe('mOne'); 233 | }); 234 | 235 | it('When the list is empty the parent must handle dragenter events', () => { 236 | let singleList:Array = ['sOne', 'sTwo', 'sThree']; 237 | let multiOneList:Array = []; 238 | let multiTwoList:Array = ['mOne', 'mTwo', 'mThree', 'mFour', 'mFive', 'mSix']; 239 | 240 | container.singleList = singleList; 241 | container.multiOneList = multiOneList; 242 | container.multiTwoList = multiTwoList; 243 | componentFixture.detectChanges(); 244 | 245 | let divElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('div'); 246 | expect(divElem).toBeDefined(); 247 | expect(divElem.children.length).toBe(3); 248 | 249 | let multiOneElem:HTMLElement = divElem.querySelector('#multiOne'); 250 | let multiTwoUlElem:HTMLElement = divElem.querySelector('#multiTwo ul'); 251 | 252 | triggerEvent(multiTwoUlElem.children[3], 'dragstart', 'MouseEvent'); 253 | triggerEvent(multiOneElem, 'dragenter', 'MouseEvent'); 254 | componentFixture.detectChanges(); 255 | 256 | expect(multiOneList.length).toBe(1); 257 | expect(multiTwoList.length).toBe(5); 258 | 259 | expect(multiTwoList[3]).toBe('mFive'); 260 | expect(multiOneList[0]).toBe('mFour'); 261 | }); 262 | 263 | }); 264 | 265 | function swap(nodes:HTMLCollection, firstNodeId:number, secondNodeId:number) { 266 | swapMultiple(nodes, firstNodeId, nodes, secondNodeId); 267 | } 268 | 269 | function swapMultiple(nodesOne:HTMLCollection, firstNodeId:number, nodesTwo:HTMLCollection, secondNodeId:number) { 270 | triggerEvent(nodesOne[firstNodeId], 'dragstart', 'MouseEvent'); 271 | triggerEvent(nodesTwo[secondNodeId], 'dragenter', 'MouseEvent'); 272 | } 273 | 274 | -------------------------------------------------------------------------------- /tests/dnd.with.draggable.data.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed, ComponentFixture } 2 | from '@angular/core/testing'; 3 | 4 | import {DragDropConfig} from '../src/dnd.config'; 5 | import {DraggableComponent} from '../src/draggable.component'; 6 | import {DroppableComponent} from '../src/droppable.component'; 7 | import {DragDropService} from '../src/dnd.service'; 8 | 9 | import {Container2, triggerEvent} from './dnd.component.factory'; 10 | 11 | describe('Drag and Drop with draggable data', () => { 12 | 13 | let componentFixture: ComponentFixture; 14 | let dragdropService: DragDropService; 15 | let config: DragDropConfig; 16 | let container:Container2; 17 | 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [DraggableComponent, DroppableComponent, Container2], 21 | providers: [DragDropConfig, DragDropService] 22 | }); 23 | TestBed.compileComponents(); 24 | }); 25 | 26 | beforeEach(inject([DragDropConfig, DragDropService], 27 | (c: DragDropConfig, dd: DragDropService) => { 28 | dragdropService = dd; 29 | config = c; 30 | 31 | componentFixture = TestBed.createComponent(Container2); 32 | componentFixture.detectChanges(); 33 | container = componentFixture.componentInstance; 34 | })); 35 | 36 | it('should be defined', () => { 37 | expect(componentFixture).toBeDefined(); 38 | }); 39 | 40 | it('It should add the "draggable" attribute', (done:any) => { 41 | let dragElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dragId'); 42 | 43 | expect(dragElem).toBeDefined(); 44 | expect(dragElem.attributes['draggable']).toBeTruthy(); 45 | 46 | done(); 47 | }); 48 | 49 | it('Drag events should add/remove the draggable data to/from the DragDropService', (done:any) => { 50 | let dragElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dragId'); 51 | 52 | expect(dragdropService.dragData).not.toBeDefined(); 53 | 54 | triggerEvent(dragElem, 'dragstart', 'MouseEvent'); 55 | componentFixture.detectChanges(); 56 | expect(dragdropService.dragData).toBeDefined(); 57 | 58 | triggerEvent(dragElem, 'dragend', 'MouseEvent'); 59 | componentFixture.detectChanges(); 60 | expect(dragdropService.dragData).toBeNull(); 61 | 62 | done(); 63 | }); 64 | 65 | it('Drag events should add/remove the expected classes to the target element', (done:any) => { 66 | let dragElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dragId'); 67 | 68 | expect(dragElem.classList.contains(config.onDragStartClass)).toEqual(false); 69 | 70 | triggerEvent(dragElem, 'dragstart', 'MouseEvent'); 71 | componentFixture.detectChanges(); 72 | expect(dragElem.classList.contains(config.onDragStartClass)).toEqual(true); 73 | 74 | triggerEvent(dragElem, 'dragend', 'MouseEvent'); 75 | componentFixture.detectChanges(); 76 | expect(dragElem.classList.contains(config.onDragStartClass)).toEqual(false); 77 | 78 | done(); 79 | }); 80 | 81 | it('Drag start event should not be activated if drag is not enabled', (done:any) => { 82 | container.dragEnabled = false; 83 | componentFixture.detectChanges(); 84 | 85 | let dragElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dragId'); 86 | 87 | expect(dragdropService.dragData).not.toBeDefined(); 88 | expect(dragElem.classList.contains(config.onDragStartClass)).toEqual(false); 89 | 90 | triggerEvent(dragElem, 'dragstart', 'MouseEvent'); 91 | componentFixture.detectChanges(); 92 | expect(dragdropService.dragData).not.toBeDefined(); 93 | expect(dragElem.classList.contains(config.onDragStartClass)).toEqual(false); 94 | 95 | done(); 96 | }); 97 | 98 | it('Drop events should add/remove the expected classes to the target element', (done:any) => { 99 | let dragElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dragId'); 100 | let dropElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dropId'); 101 | 102 | expect(dropElem.classList.contains(config.onDragEnterClass)).toEqual(false); 103 | expect(dropElem.classList.contains(config.onDragOverClass)).toEqual(false); 104 | 105 | // The drop events should not work before a drag is started on an element with the correct drop-zone 106 | triggerEvent(dropElem, 'dragenter', 'MouseEvent'); 107 | componentFixture.detectChanges(); 108 | expect(dropElem.classList.contains(config.onDragEnterClass)).toEqual(false); 109 | 110 | triggerEvent(dragElem, 'dragstart', 'MouseEvent'); 111 | triggerEvent(dropElem, 'dragenter', 'MouseEvent'); 112 | componentFixture.detectChanges(); 113 | expect(dropElem.classList.contains(config.onDragEnterClass)).toEqual(true); 114 | expect(dropElem.classList.contains(config.onDragOverClass)).toEqual(false); 115 | 116 | triggerEvent(dropElem, 'dragover', 'MouseEvent'); 117 | componentFixture.detectChanges(); 118 | expect(dropElem.classList.contains(config.onDragEnterClass)).toEqual(true); 119 | expect(dropElem.classList.contains(config.onDragOverClass)).toEqual(true); 120 | 121 | triggerEvent(dropElem, 'dragleave', 'MouseEvent'); 122 | componentFixture.detectChanges(); 123 | expect(dropElem.classList.contains(config.onDragEnterClass)).toEqual(false); 124 | expect(dropElem.classList.contains(config.onDragOverClass)).toEqual(false); 125 | 126 | triggerEvent(dropElem, 'dragover', 'MouseEvent'); 127 | triggerEvent(dropElem, 'dragenter', 'MouseEvent'); 128 | triggerEvent(dropElem, 'drop', 'MouseEvent'); 129 | componentFixture.detectChanges(); 130 | expect(dropElem.classList.contains(config.onDragEnterClass)).toEqual(false); 131 | expect(dropElem.classList.contains(config.onDragOverClass)).toEqual(false); 132 | 133 | done(); 134 | }); 135 | 136 | it('Drop event should activate the onDropSuccess and onDragSuccess callbacks', (done:any) => { 137 | let dragElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dragId'); 138 | let dropElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dropId'); 139 | 140 | let dragCount:number = 0, dropCount:number = 0; 141 | container.drag.subscribe(($event:any) => { 142 | dragCount++; 143 | }, (error:any) => {}, () => { 144 | // Here is a function called when stream is complete 145 | expect(dragCount).toBe(0); 146 | }); 147 | 148 | container.drop.subscribe(($event:any) => { 149 | dropCount++; 150 | }, (error:any) => {}, () => { 151 | // Here is a function called when stream is complete 152 | expect(dropCount).toBe(0); 153 | }); 154 | 155 | triggerEvent(dragElem, 'dragstart', 'MouseEvent'); 156 | triggerEvent(dragElem, 'dragend', 'MouseEvent'); 157 | triggerEvent(dragElem, 'dragstart', 'MouseEvent'); 158 | triggerEvent(dropElem, 'drop', 'MouseEvent'); 159 | componentFixture.detectChanges(); 160 | 161 | done(); 162 | }); 163 | 164 | it('The onDropSuccess callback should receive the dragged data as paramenter', (done: any) => { 165 | let dragData = {id: 1, name:'Hello'}; 166 | 167 | container.dragData = dragData; 168 | componentFixture.detectChanges(); 169 | 170 | let dragElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dragId'); 171 | let dropElem:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dropId'); 172 | 173 | container.drag.subscribe(($event: any) => { 174 | expect($event.dragData).toBe(dragData); 175 | }); 176 | container.drop.subscribe(($event: any) => { 177 | expect($event.dragData).toBe(dragData); 178 | }); 179 | 180 | triggerEvent(dragElem, 'dragstart', 'MouseEvent'); 181 | triggerEvent(dropElem, 'drop', 'MouseEvent'); 182 | componentFixture.detectChanges(); 183 | 184 | done(); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /tests/dnd.without.draggable.data.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed, ComponentFixture } 2 | from '@angular/core/testing'; 3 | 4 | import {DragDropConfig} from '../src/dnd.config'; 5 | import {DraggableComponent} from '../src/draggable.component'; 6 | import {DroppableComponent} from '../src/droppable.component'; 7 | import {DragDropService} from '../src/dnd.service'; 8 | 9 | import {Container, triggerEvent} from './dnd.component.factory'; 10 | 11 | describe('Drag and Drop without draggable data', () => { 12 | 13 | let componentFixture: ComponentFixture; 14 | let dragdropService: DragDropService; 15 | let config: DragDropConfig; 16 | let container:Container; 17 | 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [DraggableComponent, DroppableComponent, Container], 21 | providers: [DragDropConfig, DragDropService] 22 | }); 23 | TestBed.compileComponents(); 24 | }); 25 | 26 | beforeEach(inject([DragDropConfig, DragDropService], 27 | (c: DragDropConfig, dd: DragDropService) => { 28 | dragdropService = dd; 29 | config = c; 30 | 31 | componentFixture = TestBed.createComponent(Container); 32 | componentFixture.detectChanges(); 33 | container = componentFixture.componentInstance; 34 | })); 35 | 36 | it('should be defined', () => { 37 | expect(componentFixture).toBeDefined(); 38 | }); 39 | 40 | it('Drop events should not be activated on the wrong drop-zone', (done:any) => { 41 | let dragElemOne:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dragIdOne'); 42 | let dropElemTwo:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dropIdTwo'); 43 | 44 | triggerEvent(dragElemOne, 'dragstart', 'MouseEvent'); 45 | triggerEvent(dropElemTwo, 'dragenter', 'MouseEvent'); 46 | componentFixture.detectChanges(); 47 | expect(dropElemTwo.classList.contains(config.onDragEnterClass)).toEqual(false); 48 | 49 | triggerEvent(dropElemTwo, 'dragover', 'MouseEvent'); 50 | componentFixture.detectChanges(); 51 | expect(dropElemTwo.classList.contains(config.onDragOverClass)).toEqual(false); 52 | 53 | let dragCount:number = 0, dropCount:number = 0; 54 | container.dragOne.subscribe(($event:any) => { 55 | dragCount++; 56 | }, (error:any) => {}, () => { 57 | // Here is a function called when stream is complete 58 | expect(dragCount).toBe(0); 59 | }); 60 | 61 | container.dropTwo.subscribe(($event:any) => { 62 | dropCount++; 63 | }, (error:any) => {}, () => { 64 | // Here is a function called when stream is complete 65 | expect(dropCount).toBe(0); 66 | }); 67 | triggerEvent(dropElemTwo, 'drop', 'MouseEvent'); 68 | componentFixture.detectChanges(); 69 | 70 | done(); 71 | }); 72 | 73 | it('Drop events should be activated on the same drop-zone', (done:any) => { 74 | let dragElemOne:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dragIdOne'); 75 | let dropElemOne:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dropIdOne'); 76 | 77 | triggerEvent(dragElemOne, 'dragstart', 'MouseEvent'); 78 | triggerEvent(dropElemOne, 'dragenter', 'MouseEvent'); 79 | componentFixture.detectChanges(); 80 | expect(dropElemOne.classList.contains(config.onDragEnterClass)).toEqual(true); 81 | 82 | triggerEvent(dropElemOne, 'dragover', 'MouseEvent'); 83 | componentFixture.detectChanges(); 84 | expect(dropElemOne.classList.contains(config.onDragOverClass)).toEqual(true); 85 | 86 | let dragCount:number = 0, dropCount:number = 0; 87 | container.dragOne.subscribe(($event:any) => { 88 | dragCount++; 89 | }, (error:any) => {}, () => { 90 | // Here is a function called when stream is complete 91 | expect(dragCount).toBe(1); 92 | }); 93 | 94 | container.dropOne.subscribe(($event:any) => { 95 | dropCount++; 96 | }, (error:any) => {}, () => { 97 | // Here is a function called when stream is complete 98 | expect(dropCount).toBe(1); 99 | }); 100 | triggerEvent(dropElemOne, 'drop', 'MouseEvent'); 101 | componentFixture.detectChanges(); 102 | 103 | done(); 104 | }); 105 | 106 | it('Drop events on multiple drop-zone', (done:any) => { 107 | let dragElemOneTwo:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dragIdOneTwo'); 108 | let dropElemOneTwo:HTMLElement = componentFixture.elementRef.nativeElement.querySelector('#dropIdOneTwo'); 109 | 110 | triggerEvent(dragElemOneTwo, 'dragstart', 'MouseEvent'); 111 | triggerEvent(dropElemOneTwo, 'dragenter', 'MouseEvent'); 112 | componentFixture.detectChanges(); 113 | expect(dropElemOneTwo.classList.contains(config.onDragEnterClass)).toEqual(true); 114 | 115 | triggerEvent(dropElemOneTwo, 'dragover', 'MouseEvent'); 116 | componentFixture.detectChanges(); 117 | expect(dropElemOneTwo.classList.contains(config.onDragOverClass)).toEqual(true); 118 | 119 | let dragCount:number = 0, dropCount:number = 0; 120 | container.dragOne.subscribe(($event:any) => { 121 | dragCount++; 122 | }, (error:any) => {}, () => { 123 | // Here is a function called when stream is complete 124 | expect(dragCount).toBe(1); 125 | }); 126 | 127 | container.dropOne.subscribe(($event:any) => { 128 | dropCount++; 129 | }, (error:any) => {}, () => { 130 | // Here is a function called when stream is complete 131 | expect(dropCount).toBe(1); 132 | }); 133 | triggerEvent(dropElemOneTwo, 'drop', 'MouseEvent'); 134 | componentFixture.detectChanges(); 135 | 136 | done(); 137 | }); 138 | 139 | }); 140 | 141 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "module": "es2015", 5 | "target": "es5", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "declaration": true, 9 | "moduleResolution": "node", 10 | "types": [ 11 | "hammerjs", 12 | "jasmine", 13 | "node" 14 | ], 15 | "lib": ["es2015", "dom"] 16 | }, 17 | "files": [ 18 | "public_api.ts" 19 | ], 20 | "exclude": [ 21 | "node_modules" 22 | ], 23 | "angularCompilerOptions": { 24 | "strictMetadataEmit": true, 25 | "skipTemplateCodegen": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "curly": true, 8 | "forin": true, 9 | "indent": [ 10 | true, 11 | "spaces" 12 | ], 13 | "label-position": true, 14 | "member-access": false, 15 | "no-arg": true, 16 | "no-bitwise": true, 17 | "no-console": [ 18 | true, 19 | "debug", 20 | "info", 21 | "time", 22 | "timeEnd", 23 | "trace" 24 | ], 25 | "no-construct": true, 26 | "no-debugger": true, 27 | "no-duplicate-variable": true, 28 | "no-empty": false, 29 | "no-eval": true, 30 | "no-inferrable-types": false, 31 | "no-shadowed-variable": true, 32 | "no-string-literal": false, 33 | "no-unused-expression": true, 34 | "no-unused-variable": false, 35 | "object-literal-sort-keys": false, 36 | "one-line": [ 37 | true, 38 | "check-open-brace", 39 | "check-catch", 40 | "check-else", 41 | "check-whitespace" 42 | ], 43 | "radix": true, 44 | "semicolon": [ 45 | "always" 46 | ], 47 | "triple-equals": [ 48 | true, 49 | "allow-null-check" 50 | ], 51 | "typedef-whitespace": [ 52 | true, 53 | { 54 | "call-signature": "nospace", 55 | "index-signature": "nospace", 56 | "parameter": "nospace", 57 | "property-declaration": "nospace", 58 | "variable-declaration": "nospace" 59 | } 60 | ], 61 | "variable-name": false, 62 | // The rule have the following arguments: 63 | // [ENABLED, "attribute" | "element", "selectorPrefix" | ["listOfPrefixes"], "camelCase" | "kebab-case"] 64 | "directive-selector": [true, "attribute", "", "kebab-case"], 65 | "component-selector": [true, "element", "", "kebab-case"], 66 | "use-input-property-decorator": true, 67 | "use-output-property-decorator": true, 68 | "use-host-property-decorator": false, 69 | "use-life-cycle-interface": true, 70 | "use-pipe-transform-interface": true 71 | } 72 | } 73 | --------------------------------------------------------------------------------