├── .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 [](https://badge.fury.io/js/ng2-dnd) [](https://www.npmjs.com/package/ng2-dnd)
2 | Angular 2 Drag-and-Drop without dependencies.
3 |
4 | Follow me [](https://twitter.com/akopkokhyants) to be notified about new releases.
5 |
6 | [](https://travis-ci.org/akserg/ng2-dnd)
7 | [](https://david-dm.org/akserg/ng2-dnd)
8 | [](https://david-dm.org/akserg/ng2-dnd#info=devDependencies)
9 | [](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 |
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 |
0" *ngFor="let data of receivedData">{{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 |
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 |
0" [dragData]="product" (onDragSuccess)="orderedProduct($event)" [dropZones]="['demo1']">
362 |
363 |
{{product.name}} - \${{product.cost}} (available: {{product.quantity}})
364 |
0">{{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 |
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 |
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 |
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 |
2 |
3 |
4 |
5 |
6 |
16 |
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 |
0" *ngFor="let data of receivedData">{{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 |
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 |
0" [dragData]="product" (onDragSuccess)="orderedProduct($event)" [dropZones]="['demo1']">
15 |
16 |
{{product.name}} - \${{product.cost}} (available: {{product.quantity}})
17 |
0">{{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 | `
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 |
22 |
23 |
24 |
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 |
18 |
19 |
20 |
21 |
22 |
23 | First Team
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 | Second Team
36 |
37 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
94 | `
95 | })
96 | export class Container3 {
97 | @Input() sortableList:Array = [];
98 | }
99 |
100 | @Component({
101 | selector: 'test-container-four',
102 | template: `
103 |
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 |
--------------------------------------------------------------------------------