Repository: reppners/ngx-drag-drop Branch: master Commit: b888494da3a0 Files: 89 Total size: 147.4 KB Directory structure: gitextract_jzzxhiff/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── Bug_report.md │ │ └── Feature_request.md │ └── workflows/ │ ├── ci.yml │ ├── deploy-pages.yml │ └── publish.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── LICENSE ├── README.md ├── angular.json ├── package.json ├── pnpm-workspace.yaml ├── projects/ │ ├── demo/ │ │ ├── .browserslistrc │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── app.component.html │ │ │ │ ├── app.component.scss │ │ │ │ ├── app.component.ts │ │ │ │ ├── app.module.ts │ │ │ │ ├── demo-link/ │ │ │ │ │ ├── demo-link.component.html │ │ │ │ │ ├── demo-link.component.scss │ │ │ │ │ └── demo-link.component.ts │ │ │ │ ├── indirect-dnd-handle/ │ │ │ │ │ ├── indirect-dnd-handle.component.html │ │ │ │ │ ├── indirect-dnd-handle.component.scss │ │ │ │ │ └── indirect-dnd-handle.component.ts │ │ │ │ ├── indirect-drag-image/ │ │ │ │ │ ├── indirect-drag-image.component.html │ │ │ │ │ ├── indirect-drag-image.component.scss │ │ │ │ │ └── indirect-drag-image.component.ts │ │ │ │ ├── issue-195/ │ │ │ │ │ ├── issue-195.component.html │ │ │ │ │ ├── issue-195.component.scss │ │ │ │ │ └── issue-195.component.ts │ │ │ │ ├── list/ │ │ │ │ │ ├── list.component.html │ │ │ │ │ ├── list.component.scss │ │ │ │ │ └── list.component.ts │ │ │ │ ├── native/ │ │ │ │ │ ├── native.component.html │ │ │ │ │ ├── native.component.scss │ │ │ │ │ └── native.component.ts │ │ │ │ ├── nested/ │ │ │ │ │ ├── nested.component.html │ │ │ │ │ ├── nested.component.scss │ │ │ │ │ └── nested.component.ts │ │ │ │ ├── shadow-dom/ │ │ │ │ │ ├── shadow-dom.component.html │ │ │ │ │ ├── shadow-dom.component.scss │ │ │ │ │ └── shadow-dom.component.ts │ │ │ │ ├── simple/ │ │ │ │ │ ├── simple.component.html │ │ │ │ │ ├── simple.component.scss │ │ │ │ │ └── simple.component.ts │ │ │ │ ├── tree/ │ │ │ │ │ ├── tree.component.html │ │ │ │ │ ├── tree.component.scss │ │ │ │ │ └── tree.component.ts │ │ │ │ └── typed/ │ │ │ │ ├── typed.component.html │ │ │ │ ├── typed.component.scss │ │ │ │ └── typed.component.ts │ │ │ ├── assets/ │ │ │ │ └── .gitkeep │ │ │ ├── dragdroptouch.d.ts │ │ │ ├── environments/ │ │ │ │ ├── environment.prod.ts │ │ │ │ └── environment.ts │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ ├── polyfills.ts │ │ │ └── styles.scss │ │ ├── tsconfig.app.json │ │ └── tsconfig.spec.json │ └── dnd/ │ ├── .browserslistrc │ ├── README.md │ ├── ng-package.json │ ├── package.json │ ├── src/ │ │ ├── lib/ │ │ │ ├── dnd-draggable.directive.spec.ts │ │ │ ├── dnd-draggable.directive.ts │ │ │ ├── dnd-dropzone.directive.spec.ts │ │ │ ├── dnd-dropzone.directive.ts │ │ │ ├── dnd-handle.directive.spec.ts │ │ │ ├── dnd-handle.directive.ts │ │ │ ├── dnd-state.spec.ts │ │ │ ├── dnd-state.ts │ │ │ ├── dnd-types.ts │ │ │ ├── dnd-utils.spec.ts │ │ │ ├── dnd-utils.ts │ │ │ └── dnd.module.ts │ │ ├── public-api.ts │ │ └── test-setup.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── renovate.json ├── tsconfig.json └── vitest.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: .github/ISSUE_TEMPLATE/Bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** If possible please provide a StackBlitz based on this template https://stackblitz.com/edit/ngx-drag-drop-issue-template Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/Feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request: branches: [master] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: pnpm/action-setup@71c92474e7e4f5bca283fb17ef80fba9cdb2b4b1 # v6 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm exec prettier --check . test: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: pnpm/action-setup@71c92474e7e4f5bca283fb17ef80fba9cdb2b4b1 # v6 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm run build:lib - name: Run tests with coverage run: | echo '## Test Coverage' >> $GITHUB_STEP_SUMMARY echo '' >> $GITHUB_STEP_SUMMARY pnpm run test:coverage 2>&1 | tee /dev/stderr | sed -n '/^-.*|/,/^-.*|$/p' >> $GITHUB_STEP_SUMMARY build: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: pnpm/action-setup@71c92474e7e4f5bca283fb17ef80fba9cdb2b4b1 # v6 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm run build:lib - run: pnpm run build:docs ================================================ FILE: .github/workflows/deploy-pages.yml ================================================ name: Deploy Demo to GitHub Pages on: push: branches: [master] workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: pages cancel-in-progress: true jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: pnpm/action-setup@71c92474e7e4f5bca283fb17ef80fba9cdb2b4b1 # v6 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm run build:lib - run: pnpm run build:docs - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 with: path: docs deploy: needs: build runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish to npm on: release: types: [published] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: pnpm/action-setup@71c92474e7e4f5bca283fb17ef80fba9cdb2b4b1 # v6 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm run build:lib - name: Run tests with coverage run: | echo '## Test Coverage' >> $GITHUB_STEP_SUMMARY echo '' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY pnpm run test:coverage 2>&1 | tee /dev/stderr | sed -n '/^-.*|/,/^-.*|$/p' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY publish: needs: test runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: pnpm/action-setup@71c92474e7e4f5bca283fb17ef80fba9cdb2b4b1 # v6 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 with: node-version: 24 cache: pnpm registry-url: https://registry.npmjs.org/ - run: pnpm install --frozen-lockfile - run: pnpm run build:lib - run: pnpm publish ./dist/ngx-drag-drop --provenance --access public --no-git-checks ================================================ FILE: .gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # compiled output /dist /docs /tmp /out-tsc # Only exists if Bazel was run /bazel-out /.ng_build # dependencies /node_modules # profiling files chrome-profiler-events*.json # IDEs and editors /.idea .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # IDE - VSCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # misc /.angular/cache /.sass-cache /connect.lock /coverage /libpeerconnection.log npm-debug.log pnpm-debug.log package-lock.json yarn-error.log testem.log /typings # System Files .DS_Store Thumbs.db node_modules ================================================ FILE: .prettierignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # compiled output /dist /docs /tmp /out-tsc # Only exists if Bazel was run /bazel-out /.ng_build /.github # dependencies /node_modules # profiling files chrome-profiler-events*.json # IDEs and editors /.idea .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # IDE - VSCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # misc /.angular/cache /.sass-cache /connect.lock /coverage /libpeerconnection.log npm-debug.log pnpm-debug.log pnpm-lock.yaml yarn-error.log testem.log /typings # System Files .DS_Store Thumbs.db node_modules ================================================ FILE: .prettierrc.json ================================================ { "tabWidth": 2, "useTabs": false, "singleQuote": true, "semi": true, "bracketSpacing": true, "arrowParens": "avoid", "trailingComma": "es5", "bracketSameLine": true, "printWidth": 80 } ================================================ FILE: .vscode/extensions.json ================================================ { // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 "recommendations": ["angular.ng-template"] } ================================================ FILE: .vscode/launch.json ================================================ { // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "ng serve", "type": "pwa-chrome", "request": "launch", "preLaunchTask": "npm: start", "url": "http://localhost:4200/" }, { "name": "ng test", "type": "chrome", "request": "launch", "preLaunchTask": "npm: test", "url": "http://localhost:9876/debug.html" } ] } ================================================ FILE: .vscode/tasks.json ================================================ { // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 "version": "2.0.0", "tasks": [ { "type": "npm", "script": "start", "isBackground": true, "problemMatcher": { "owner": "typescript", "pattern": "$tsc", "background": { "activeOnStart": true, "beginsPattern": { "regexp": "(.*?)" }, "endsPattern": { "regexp": "bundle generation complete" } } } }, { "type": "npm", "script": "test", "isBackground": true, "problemMatcher": { "owner": "typescript", "pattern": "$tsc", "background": { "activeOnStart": true, "beginsPattern": { "regexp": "(.*?)" }, "endsPattern": { "regexp": "bundle generation complete" } } } } ] } ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2021, Stefan Steinhart All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ [![npm](https://img.shields.io/npm/v/ngx-drag-drop.svg)](https://www.npmjs.com/package/ngx-drag-drop) [![npm (next)](https://img.shields.io/npm/v/ngx-drag-drop/next.svg)](https://www.npmjs.com/package/ngx-drag-drop) [![NpmLicense](https://img.shields.io/npm/l/ngx-drag-drop.svg)](https://www.npmjs.com/package/ngx-drag-drop) [![GitHub issues](https://img.shields.io/github/issues/ChristofFritz/ngx-drag-drop.svg)](https://github.com/ChristofFritz/ngx-drag-drop/issues) [![Twitter](https://img.shields.io/twitter/url/https/github.com/ChristofFritz/ngx-drag-drop.svg?style=social)](https://twitter.com/intent/tweet?text=Angular%20drag%20and%20drop%20with%20ease:&url=https://github.com/ChristofFritz/ngx-drag-drop) # NgxDragDrop [_Demo_](https://christoffritz.github.io/ngx-drag-drop/) / [_StackBlitz Issue Template_](https://stackblitz.com/edit/ngx-drag-drop-issue-template) ```sh npm install ngx-drag-drop # or pnpm add ngx-drag-drop ``` **Angular directives for declarative drag and drop using the HTML5 Drag-And-Drop API** - sortable lists by using placeholder element (vertical and horizontal) - nestable - dropzones optionally support external/native draggables (img, txt, file) - conditional drag/drop - typed drag/drop - utilize [EffectAllowed](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/effectAllowed) - custom CSS classes - touch support by using a [polyfill](#touch-support) - [AOT](https://angular.io/guide/aot-compiler) compatible Port of [angular-drag-drop-lists](https://github.com/marceljuenemann/angular-drag-and-drop-lists) but without the lists :wink: This has `dropzones` though :+1: The idea is that the directive does not handle lists internally so the `dndDropzone` can be general purpose. ## Angular Version Compatibility Starting with v13, the library major version matches the Angular major version. | Angular | ngx-drag-drop | | ------- | ------------- | | 21.x | 21.x | | 20.x | 20.x | | 19.x | 19.x | | 18.x | 18.x | | 17.x | 17.x | | 16.x | 16.x | | 15.x | 15.x | | 14.x | 14.x | | 13.x | 13.x | For older Angular versions (v4–v12), use ngx-drag-drop v2.x. ## Usage `app.component.html` ```HTML
HANDLE
draggable ({{draggable.effectAllowed}}) DISABLED
DRAG_IMAGE
dropzone
placeholder
``` `app.component` ```JS import { Component } from '@angular/core'; import { DndDropEvent } from 'ngx-drag-drop'; @Component() export class AppComponent { draggable = { // note that data is handled with JSON.stringify/JSON.parse // only set simple data or POJO's as methods will be lost data: "myDragData", effectAllowed: "all", disable: false, handle: false }; onDragStart(event:DragEvent) { console.log("drag started", JSON.stringify(event, null, 2)); } onDragEnd(event:DragEvent) { console.log("drag ended", JSON.stringify(event, null, 2)); } onDraggableCopied(event:DragEvent) { console.log("draggable copied", JSON.stringify(event, null, 2)); } onDraggableLinked(event:DragEvent) { console.log("draggable linked", JSON.stringify(event, null, 2)); } onDraggableMoved(event:DragEvent) { console.log("draggable moved", JSON.stringify(event, null, 2)); } onDragCanceled(event:DragEvent) { console.log("drag cancelled", JSON.stringify(event, null, 2)); } onDragover(event:DragEvent) { console.log("dragover", JSON.stringify(event, null, 2)); } onDrop(event:DndDropEvent) { console.log("dropped", JSON.stringify(event, null, 2)); } } ``` `app.module` ```JS import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { DndModule } from 'ngx-drag-drop'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, DndModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } ``` ## API ```TS // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/dropEffect export type DropEffect = "move" | "copy" | "link" | "none"; // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/effectAllowed export type EffectAllowed = DropEffect | "copyMove" | "copyLink" | "linkMove" | "all"; ``` ```TS export type DndDragImageOffsetFunction = ( event:DragEvent, dragImage:Element ) => { x:number, y:number }; @Directive( { selector: "[dndDraggable]" } ) export declare class DndDraggableDirective { // the data attached to the drag dndDraggable: any; // the allowed drop effect dndEffectAllowed: EffectAllowed; // optionally set the type of dragged data to restrict dropping on compatible dropzones dndType?: string; // conditionally disable the draggability dndDisableIf: boolean; dndDisableDragIf: boolean; // set a custom class that is applied while dragging dndDraggingClass: string = "dndDragging"; // set a custom class that is applied to only the src element while dragging dndDraggingSourceClass: string = "dndDraggingSource"; // set the class that is applied when draggable is disabled by [dndDisableIf] dndDraggableDisabledClass = "dndDraggableDisabled"; // enables to set a function for calculating custom dragimage offset dndDragImageOffsetFunction:DndDragImageOffsetFunction = calculateDragImageOffset; // emits on drag start readonly dndStart: EventEmitter; // emits on drag readonly dndDrag: EventEmitter; // emits on drag end readonly dndEnd: EventEmitter; // emits when the dragged item has been dropped with effect "move" readonly dndMoved: EventEmitter; // emits when the dragged item has been dropped with effect "copy" readonly dndCopied: EventEmitter; // emits when the dragged item has been dropped with effect "link" readonly dndLinked: EventEmitter; // emits when the drag is canceled readonly dndCanceled: EventEmitter; } ``` ```TS export interface DndDropEvent { // the original drag event event: DragEvent; // the actual drop effect dropEffect: DropEffect; // true if the drag did not origin from a [dndDraggable] isExternal:boolean; // the data set on the [dndDraggable] that started the drag // for external drags use the event property which contains the original drop event as this will be undefined data?: any; // the index where the draggable was dropped in a dropzone // set only when using a placeholder index?: number; // if the dndType input on dndDraggable was set // it will be transported here type?: any; } @Directive( { selector: "[dndDropzone]" } ) export declare class DndDropzoneDirective { // optionally restrict the allowed types dndDropzone?: string[]; // set the allowed drop effect dndEffectAllowed: EffectAllowed; // conditionally disable the dropzone dndDisableIf: boolean; dndDisableDropIf: boolean; // if draggables that are not [dndDraggable] are allowed to be dropped // set to true if dragged text, images or files should be handled dndAllowExternal: boolean; // if its a horizontal list this influences how the placeholder position // is calculated dndHorizontal: boolean; // set the class applied to the dropzone // when a draggable is dragged over it dndDragoverClass: string = "dndDragover"; // set the class applied to the dropzone // when the dropzone is disabled by [dndDisableIf] dndDropzoneDisabledClass = "dndDropzoneDisabled"; // emits when a draggable is dragged over the dropzone readonly dndDragover: EventEmitter; // emits on successful drop readonly dndDrop: EventEmitter; } ``` ## Touch support (experimental) This library uses the native HTML5 Drag and Drop API, which is **not supported on most mobile browsers** (including iOS Safari). Touch support requires a polyfill that translates touch events into drag events. This approach has known limitations and the experience may not be reliable on all devices. To enable basic touch support, install the `@dragdroptouch/drag-drop-touch` polyfill: ```JS import { enableDragDropTouch } from "@dragdroptouch/drag-drop-touch"; enableDragDropTouch(); ``` For more info on the polyfill check it out on GitHub https://github.com/drag-drop-touch-js/dragdroptouch ## Known issues ### Firefox - Beware that Firefox does not support dragging on ` @for (demo of issueDemos; track demo.issue) { } @if (activeTab$ | async; as activeTab) { } ================================================ FILE: projects/demo/src/app/app.component.scss ================================================ :host { display: flex; flex: 1 1 auto; flex-direction: column; max-height: 100vh; max-width: 100vw; height: 100%; width: 100%; } .container-padding { padding: 8px; } .scrollable { overflow: auto; -webkit-overflow-scrolling: touch; } /* workaround for flex tab content */ /* TODO(mdc-migration): The following rule targets internal classes of tabs that may no longer apply for the MDC version. */ mat-tab-group ::ng-deep .mat-tab-body-wrapper { height: 100%; flex: 1 1 auto; } /* TODO(mdc-migration): The following rule targets internal classes of tabs that may no longer apply for the MDC version. */ mat-tab-group ::ng-deep .mat-tab-body-wrapper .mat-tab-body-content { display: flex; height: 100%; flex: 1 1 auto; } ================================================ FILE: projects/demo/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { ActivatedRoute, ActivationEnd, Router } from '@angular/router'; import { filter, map, Observable, shareReplay, startWith } from 'rxjs'; const TABS: string[] = [ 'simple', 'list', 'nested', 'tree', 'native', 'typed', 'shadow-dom', ]; const ISSUE_DEMOS: { issue: number; label: string }[] = [ { issue: 195, label: '#195 — dropEffect ignores dropzone effectAllowed' }, ]; const DEFAULT_TAB = TABS[0]; @Component({ selector: 'dnd-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], standalone: false, }) export class AppComponent { readonly title = 'NgxDragDrop Demo'; readonly tabs: string[] = TABS; readonly issueDemos = ISSUE_DEMOS; readonly activeTab$: Observable; constructor( sanitizer: DomSanitizer, iconRegistry: MatIconRegistry, activatedRoute: ActivatedRoute, private router: Router ) { iconRegistry.addSvgIcon( 'github', sanitizer.bypassSecurityTrustResourceUrl('assets/github.svg') ); this.activeTab$ = this.router.events.pipe( filter(event => event instanceof ActivationEnd), map(event => { const activationEnd = event as ActivationEnd; if (!!activationEnd?.snapshot?.url?.length) { return activationEnd?.snapshot?.url[0]?.path ?? DEFAULT_TAB; } return DEFAULT_TAB; }), startWith(DEFAULT_TAB), shareReplay(1) ); } onTabLinkClick(tab: string) { this.router.navigate([tab]); } onIssueDemoClick(issue: number) { this.router.navigate(['issue', issue]); } } ================================================ FILE: projects/demo/src/app/app.module.ts ================================================ import { provideHttpClient, withInterceptorsFromDi, } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatLineModule } from '@angular/material/core'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterModule, Routes } from '@angular/router'; import { DndModule } from 'ngx-drag-drop'; import { AppComponent } from './app.component'; import { DemoLinkComponent } from './demo-link/demo-link.component'; const routes: Routes = [ { path: 'simple', loadComponent: () => import('./simple/simple.component'), }, { path: 'list', loadComponent: () => import('./list/list.component'), }, { path: 'nested', loadComponent: () => import('./nested/nested.component'), }, { path: 'tree', loadComponent: () => import('./tree/tree.component'), }, { path: 'native', loadComponent: () => import('./native/native.component'), }, { path: 'typed', loadComponent: () => import('./typed/typed.component'), }, { path: 'shadow-dom', loadComponent: () => import('./shadow-dom/shadow-dom.component'), }, { path: 'issue/195', loadComponent: () => import('./issue-195/issue-195.component'), }, { path: '**', pathMatch: 'full', redirectTo: 'simple', }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], }) export class AppRoutingModule {} @NgModule({ declarations: [AppComponent], bootstrap: [AppComponent], imports: [ BrowserModule, BrowserAnimationsModule, DndModule, MatButtonModule, MatInputModule, MatToolbarModule, MatCardModule, MatSnackBarModule, MatSlideToggleModule, MatIconModule, MatMenuModule, MatTabsModule, AppRoutingModule, MatLineModule, MatListModule, DemoLinkComponent, ], providers: [provideHttpClient(withInterceptorsFromDi())], }) export class AppModule {} ================================================ FILE: projects/demo/src/app/demo-link/demo-link.component.html ================================================ go to sources ================================================ FILE: projects/demo/src/app/demo-link/demo-link.component.scss ================================================ :host { margin: 8px 0; } ================================================ FILE: projects/demo/src/app/demo-link/demo-link.component.ts ================================================ import { Component, Input } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; @Component({ selector: 'dnd-demo-link', templateUrl: './demo-link.component.html', styleUrls: ['./demo-link.component.scss'], standalone: true, imports: [MatButtonModule], }) export class DemoLinkComponent { @Input() name: string | null = null; public get url(): string { return `https://github.com/ChristofFritz/ngx-drag-drop/tree/master/projects/demo/src/app/${this.name}`; } } ================================================ FILE: projects/demo/src/app/indirect-dnd-handle/indirect-dnd-handle.component.html ================================================ ================================================ FILE: projects/demo/src/app/indirect-dnd-handle/indirect-dnd-handle.component.scss ================================================ ================================================ FILE: projects/demo/src/app/indirect-dnd-handle/indirect-dnd-handle.component.ts ================================================ import { Component, HostBinding } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { DndHandleDirective } from 'ngx-drag-drop'; @Component({ selector: 'dnd-indirect-handle', templateUrl: './indirect-dnd-handle.component.html', styleUrls: ['./indirect-dnd-handle.component.scss'], standalone: true, imports: [MatIconModule, DndHandleDirective], }) export class IndirectDndHandleComponent { @HostBinding('class.drag-handle') get dragHandle() { return true; } } ================================================ FILE: projects/demo/src/app/indirect-drag-image/indirect-drag-image.component.html ================================================
================================================ FILE: projects/demo/src/app/indirect-drag-image/indirect-drag-image.component.scss ================================================ ================================================ FILE: projects/demo/src/app/indirect-drag-image/indirect-drag-image.component.ts ================================================ import { Component } from '@angular/core'; import { DndDragImageRefDirective } from 'ngx-drag-drop'; @Component({ selector: 'dnd-indirect-drag-image', templateUrl: './indirect-drag-image.component.html', styleUrls: ['./indirect-drag-image.component.scss'], standalone: true, imports: [DndDragImageRefDirective], }) export class IndirectDragImageComponent {} ================================================ FILE: projects/demo/src/app/issue-195/issue-195.component.html ================================================

Issue #195: dropEffect ignores dropzone's dndEffectAllowed

Draggable has dndEffectAllowed="all". Dropzone has dndEffectAllowed="link". On drop, the dropEffect should be "link", not "move".

Drag me (effectAllowed: all)
Drop here (effectAllowed: link)
@if (lastDropEvent) {
Dropzone received dropEffect: {{ lastDropEvent.dropEffect }} {{ lastDropEvent.dropEffect === 'link' ? 'PASS' : 'FAIL — expected "link"' }}
Draggable fired effect: {{ dragEffect }} {{ dragEffect === 'link' ? 'PASS' : 'FAIL — expected "link"' }}
} ================================================ FILE: projects/demo/src/app/issue-195/issue-195.component.scss ================================================ .demo { display: flex; gap: 32px; padding: 16px; align-items: flex-start; } .draggable { padding: 16px 24px; background: white; border: 2px solid #3f51b5; border-radius: 4px; cursor: move; } .dropzone { padding: 24px; min-height: 80px; min-width: 200px; background: #f5f5f5; border: 2px dashed #999; border-radius: 4px; } .placeholder { padding: 16px; border: 2px dashed orangered; border-radius: 4px; } .result { margin: 16px; padding: 12px 16px; background: #fafafa; border-radius: 4px; display: flex; gap: 16px; align-items: center; } .pass { color: green; font-weight: bold; } .fail { color: red; font-weight: bold; } ================================================ FILE: projects/demo/src/app/issue-195/issue-195.component.ts ================================================ import { JsonPipe } from '@angular/common'; import { Component } from '@angular/core'; import { DndDraggableDirective, DndDropEvent, DndDropzoneDirective, DndPlaceholderRefDirective, } from 'ngx-drag-drop'; @Component({ selector: 'dnd-issue-195', templateUrl: './issue-195.component.html', styleUrls: ['./issue-195.component.scss'], standalone: true, imports: [ DndDraggableDirective, DndDropzoneDirective, DndPlaceholderRefDirective, JsonPipe, ], }) export default class Issue195Component { lastDropEvent: DndDropEvent | null = null; dragEffect: string = ''; onDrop(event: DndDropEvent) { this.lastDropEvent = event; } } ================================================ FILE: projects/demo/src/app/list/list.component.html ================================================
dndEffectAllowed="copyMove"
@for (item of draggableListLeft; track item) { @if (item.handle) {
} {{ item.content }} effectAllowed: {{ item.effectAllowed }}
}
dndEffectAllowed="copyMove"
@for (item of draggableListRight; track item) { @if (item.handle) {
} {{ item.content }} effectAllowed: {{ item.effectAllowed }}
}
trash (dndEffectAllowed="move")
================================================ FILE: projects/demo/src/app/list/list.component.scss ================================================ :host { display: block; } pre { text-align: center; padding: 8px; } .dndList { transition: all 300ms ease; padding: 5px; &:not(.horizontal).dndDragover { padding-top: 12px; padding-bottom: 12px; } &.horizontal { &.dndDragover { padding-left: 12px; padding-right: 12px; padding-bottom: 12px; } .mat-mdc-list-item { max-width: 120px; margin: 2px; } } } .dndDragging { border: 1px solid green; } .dndDraggingSource { display: none; } .dndPlaceholder { min-height: 72px; } ================================================ FILE: projects/demo/src/app/list/list.component.ts ================================================ import { Component } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatSnackBar } from '@angular/material/snack-bar'; import { DndDraggableDirective, DndDropEvent, DndDropzoneDirective, DndHandleDirective, DndPlaceholderRefDirective, DropEffect, EffectAllowed, } from 'ngx-drag-drop'; interface DraggableItem { content: string; effectAllowed: EffectAllowed; disable: boolean; handle: boolean; } interface DropzoneLayout { container: string; list: string; dndHorizontal: boolean; } @Component({ selector: 'dnd-list', templateUrl: './list.component.html', styleUrls: ['./list.component.scss'], standalone: true, imports: [ MatListModule, DndDropzoneDirective, DndPlaceholderRefDirective, DndDraggableDirective, DndHandleDirective, MatIconModule, ], }) export default class ListComponent { draggableListLeft: DraggableItem[] = [ { content: 'Left', effectAllowed: 'move', disable: false, handle: false, }, { content: 'Lefter', effectAllowed: 'move', disable: false, handle: false, }, { content: 'Leftest', effectAllowed: 'copyMove', disable: false, handle: false, }, { content: 'Lefty', effectAllowed: 'move', disable: false, handle: true, }, { content: 'Left', effectAllowed: 'move', disable: false, handle: false, }, { content: 'Lefter', effectAllowed: 'move', disable: false, handle: false, }, { content: 'Leftest', effectAllowed: 'copyMove', disable: false, handle: false, }, { content: 'Lefty', effectAllowed: 'move', disable: false, handle: true, }, { content: 'Left', effectAllowed: 'move', disable: false, handle: false, }, { content: 'Lefter', effectAllowed: 'move', disable: false, handle: false, }, { content: 'Leftest', effectAllowed: 'copyMove', disable: false, handle: false, }, { content: 'Lefty', effectAllowed: 'move', disable: false, handle: true, }, ]; draggableListRight: DraggableItem[] = [ { content: 'I was originally right', effectAllowed: 'move', disable: false, handle: false, }, ]; horizontalLayoutActive: boolean = false; private currentDraggableEvent?: DragEvent; private currentDragEffectMsg?: string; private readonly verticalLayout: DropzoneLayout = { container: 'row', list: 'column', dndHorizontal: false, }; layout: DropzoneLayout = this.verticalLayout; private readonly horizontalLayout: DropzoneLayout = { container: 'row', list: 'row wrap', dndHorizontal: true, }; constructor(private snackBarService: MatSnackBar) {} setHorizontalLayout(horizontalLayoutActive: boolean) { this.layout = horizontalLayoutActive ? this.horizontalLayout : this.verticalLayout; } onDragStart(event: DragEvent) { this.currentDragEffectMsg = ''; this.currentDraggableEvent = event; this.snackBarService.dismiss(); this.snackBarService.open('Drag started!', undefined, { duration: 2000 }); } onDragged(item: any, list: any[], effect: DropEffect) { this.currentDragEffectMsg = `Drag ended with effect "${effect}"!`; if (effect === 'move') { const index = list.indexOf(item); list.splice(index, 1); } } onDragEnd(event: DragEvent) { this.currentDraggableEvent = event; this.snackBarService.dismiss(); this.snackBarService.open( this.currentDragEffectMsg || `Drag ended!`, undefined, { duration: 2000 } ); } onDrop(event: DndDropEvent, list?: any[]) { if (list && (event.dropEffect === 'copy' || event.dropEffect === 'move')) { let index = event.index; if (typeof index === 'undefined') { index = list.length; } list.splice(index, 0, event.data); } } } ================================================ FILE: projects/demo/src/app/native/native.component.html ================================================ ================================================ FILE: projects/demo/src/app/native/native.component.scss ================================================ :host { display: block; box-sizing: border-box; } pre { white-space: pre-wrap; word-wrap: break-word; } ================================================ FILE: projects/demo/src/app/native/native.component.ts ================================================ import { JsonPipe } from '@angular/common'; import { Component } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { MatSnackBar } from '@angular/material/snack-bar'; import { DndDropEvent, DndDropzoneDirective } from 'ngx-drag-drop'; @Component({ selector: 'dnd-native', templateUrl: './native.component.html', styleUrls: ['./native.component.scss'], standalone: true, imports: [MatCardModule, DndDropzoneDirective, JsonPipe], }) export default class NativeComponent { public lastDropEvent: DndDropEvent | null = null; public lastDropTypes?: ReadonlyArray; public lastDropFiles?: object[]; public lastDropItems?: object[]; constructor(private snackBarService: MatSnackBar) {} onDrop(event: DndDropEvent) { this.snackBarService.dismiss(); this.snackBarService.open(`Something dropped O.O`, undefined, { duration: 2000, }); this.lastDropEvent = event; this.lastDropTypes = this.lastDropEvent.event.dataTransfer?.types; this.lastDropFiles = []; this.lastDropItems = []; if (this.lastDropEvent.event.dataTransfer?.files) { for ( let i: number = 0; i < this.lastDropEvent.event.dataTransfer?.files.length; i++ ) { const file = this.lastDropEvent.event.dataTransfer?.files[i]; this.lastDropFiles.push({ lastModifiedDate: file.lastModified, name: file.name, type: file.type, size: file.size, }); } for ( let i: number = 0; i < this.lastDropEvent.event.dataTransfer.items.length; i++ ) { const item = this.lastDropEvent.event.dataTransfer.items[i]; this.lastDropItems.push({ type: item.type, kind: item.kind, data: this.lastDropEvent.event.dataTransfer.getData(item.type), }); } } } } ================================================ FILE: projects/demo/src/app/nested/nested.component.html ================================================
@for (item of list; track item) { @if (item.handle) {
drag_handle
} {{ item.content }}
@if (!!item.customDragImage) {
MY_CUSTOM_DRAG_IMAGE
} @if (item.children) {
}
}
================================================ FILE: projects/demo/src/app/nested/nested.component.scss ================================================ :host { display: block; box-sizing: border-box; } mat-card { transition: transform 200ms, opacity 200ms; } mat-card-header { border-bottom: 2px solid rgba(0, 0, 0, 0.04); align-items: center; min-height: 46px; } .dndDraggingSource { opacity: 0.5; transform: scale(0.98); } .dndPlaceholder { background: #fff; min-height: 46px; min-width: 46px; } [dnddropzone] { background: #fafafa; min-height: 60px; display: flex; outline: 2px solid rgba(0, 0, 0, 0.04); } ================================================ FILE: projects/demo/src/app/nested/nested.component.ts ================================================ import { NgTemplateOutlet } from '@angular/common'; import { Component } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBar } from '@angular/material/snack-bar'; import { DndDraggableDirective, DndDragImageRefDirective, DndDropEvent, DndDropzoneDirective, DndHandleDirective, DndPlaceholderRefDirective, DropEffect, } from 'ngx-drag-drop'; interface NestableListItem { content: string; disable?: boolean; handle?: boolean; customDragImage?: boolean; children?: NestableListItem[]; } @Component({ selector: 'dnd-nested', templateUrl: './nested.component.html', styleUrls: ['./nested.component.scss'], standalone: true, imports: [ MatCardModule, DndPlaceholderRefDirective, DndDraggableDirective, MatIconModule, DndHandleDirective, DndDragImageRefDirective, DndDropzoneDirective, NgTemplateOutlet, ], }) export default class NestedComponent { nestableList: NestableListItem[] = [ { content: 'Got something nested', children: [ { content: 'Nested', customDragImage: true, children: [], }, ], }, { content: 'No nested dropping here', }, { content: 'Got more than one', children: [ { content: 'Nested 1', handle: true, children: [], }, { content: 'Nested 2', children: [], }, { content: 'Nested 3', children: [], }, ], }, { content: "Drop something, I'll catch!", children: [], }, ]; private currentDraggableEvent?: DragEvent; private currentDragEffectMsg?: string; constructor(private snackBarService: MatSnackBar) {} onDragStart(event: DragEvent) { this.currentDragEffectMsg = ''; this.currentDraggableEvent = event; this.snackBarService.dismiss(); this.snackBarService.open('Drag started!', undefined, { duration: 2000 }); } onDragged(item: any, list: any[], effect: DropEffect) { this.currentDragEffectMsg = `Drag ended with effect "${effect}"!`; if (effect === 'move') { const index = list.indexOf(item); list.splice(index, 1); } } onDragEnd(event: DragEvent) { this.currentDraggableEvent = event; this.snackBarService.dismiss(); this.snackBarService.open( this.currentDragEffectMsg || `Drag ended!`, undefined, { duration: 2000 } ); } onDrop(event: DndDropEvent, list?: any[]) { if (list && (event.dropEffect === 'copy' || event.dropEffect === 'move')) { let index = event.index; if (typeof index === 'undefined') { index = list.length; } list.splice(index, 0, event.data); } } } ================================================ FILE: projects/demo/src/app/shadow-dom/shadow-dom.component.html ================================================
@for (item of list; track item) {
{{ item.content }}
@if (item.children) {
}
}
================================================ FILE: projects/demo/src/app/shadow-dom/shadow-dom.component.scss ================================================ :host { display: block; padding: 16px; } .root-dropzone, .nested-dropzone { display: flex; flex-direction: column; gap: 8px; padding: 8px; min-height: 40px; background: #f5f5f5; border-radius: 4px; } .nested-dropzone { margin-top: 8px; background: #ede7f6; } .drag-item { padding: 12px 16px; background: white; border: 1px solid #ddd; border-radius: 4px; cursor: move; } .drag-item-header { font-weight: 500; } .placeholder { padding: 12px 16px; border: 2px dashed orangered; border-radius: 4px; background: rgba(255, 69, 0, 0.05); } .dndDragover { outline: 2px solid #3f51b5; } ================================================ FILE: projects/demo/src/app/shadow-dom/shadow-dom.component.ts ================================================ import { NgTemplateOutlet } from '@angular/common'; import { Component, ViewEncapsulation } from '@angular/core'; import { DndDraggableDirective, DndDropEvent, DndDropzoneDirective, DndPlaceholderRefDirective, DropEffect, } from 'ngx-drag-drop'; interface NestableListItem { content: string; children?: NestableListItem[]; } @Component({ selector: 'dnd-shadow-dom', templateUrl: './shadow-dom.component.html', styleUrls: ['./shadow-dom.component.scss'], standalone: true, encapsulation: ViewEncapsulation.ShadowDom, imports: [ DndDropzoneDirective, DndPlaceholderRefDirective, DndDraggableDirective, NgTemplateOutlet, ], }) export default class ShadowDomComponent { nestableList: NestableListItem[] = [ { content: 'Parent A', children: [ { content: 'Child A.1', children: [] }, { content: 'Child A.2', children: [] }, ], }, { content: 'Parent B (no children)', }, { content: 'Parent C', children: [ { content: 'Child C.1', children: [] }, { content: 'Child C.2', children: [] }, { content: 'Child C.3', children: [] }, ], }, { content: 'Parent D (empty)', children: [], }, ]; onDragged( item: NestableListItem, list: NestableListItem[], effect: DropEffect ) { if (effect === 'move') { const index = list.indexOf(item); list.splice(index, 1); } } onDrop(event: DndDropEvent, list?: NestableListItem[]) { if (list && (event.dropEffect === 'copy' || event.dropEffect === 'move')) { let index = event.index; if (typeof index === 'undefined') { index = list.length; } list.splice(index, 0, event.data); } } } ================================================ FILE: projects/demo/src/app/simple/simple.component.html ================================================
@for (draggable of draggables; track draggable) { @if (draggable.handle) {
} draggable ({{ draggable.effectAllowed }}) only with handle DISABLED
} @if (draggableWithDragImage.handle) {
} draggable ({{ draggableWithDragImage.effectAllowed }}) only with handle DISABLED
MY_CUSTOM_DRAG_IMAGE
draggable ({{ draggableWithDragImage.effectAllowed }}) I'm the drag image but pssst
draggable ({{ draggableWithDragImage.effectAllowed }})
Dropzone Enabled
@if (lastDropEvent) {
{{ lastDropEvent | json }}
}
================================================ FILE: projects/demo/src/app/simple/simple.component.scss ================================================ :host { display: block; height: 100%; } .my-dropzone { transition: all 300ms ease; } .custom-drag-over { background-color: rgba(0, 0, 0, 0.06); } pre { white-space: pre-wrap; word-wrap: break-word; } ================================================ FILE: projects/demo/src/app/simple/simple.component.ts ================================================ import { JsonPipe } from '@angular/common'; import { Component } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSnackBar } from '@angular/material/snack-bar'; import { DndDraggableDirective, DndDragImageOffsetFunction, DndDragImageRefDirective, DndDropEvent, DndDropzoneDirective, DndHandleDirective, EffectAllowed, } from 'ngx-drag-drop'; import { IndirectDndHandleComponent } from '../indirect-dnd-handle/indirect-dnd-handle.component'; import { IndirectDragImageComponent } from '../indirect-drag-image/indirect-drag-image.component'; interface DraggableItem { content: string; effectAllowed: EffectAllowed; disable: boolean; handle: boolean; } @Component({ selector: 'dnd-simple', templateUrl: './simple.component.html', styleUrls: ['./simple.component.scss'], standalone: true, imports: [ MatCardModule, DndDraggableDirective, DndHandleDirective, MatIconModule, DndDragImageRefDirective, IndirectDndHandleComponent, IndirectDragImageComponent, MatSlideToggleModule, DndDropzoneDirective, JsonPipe, ], }) export default class SimpleComponent { draggables: DraggableItem[] = [ { content: 'testdata', effectAllowed: 'copy', disable: false, handle: false, }, { content: 'testdata2', effectAllowed: 'move', disable: false, handle: false, }, { content: 'testdata3', effectAllowed: 'link', disable: false, handle: false, }, { content: 'testdata4', effectAllowed: 'copy', disable: true, handle: false, }, { content: 'testdata5', effectAllowed: 'copy', disable: false, handle: true, }, ]; draggableWithDragImage: DraggableItem = { content: 'testdata6', effectAllowed: 'copy', disable: false, handle: true, }; public dropzoneEnabled: boolean = true; public lastDropEvent: DndDropEvent | null = null; private currentDraggableEvent?: DragEvent; private currentDragEffectMsg?: string; constructor(private snackBarService: MatSnackBar) {} dragImageOffsetRight: DndDragImageOffsetFunction = ( event: DragEvent, dragImage: Element ) => { const dragImageComputedStyle = window.getComputedStyle(dragImage); const paddingTop = parseFloat(dragImageComputedStyle.paddingTop) || 0; const paddingLeft = parseFloat(dragImageComputedStyle.paddingLeft) || 0; const borderTop = parseFloat(dragImageComputedStyle.borderTopWidth) || 0; const borderLeft = parseFloat(dragImageComputedStyle.borderLeftWidth) || 0; const x = dragImage.clientWidth - (event.offsetX + paddingLeft + borderLeft); return { x: x, y: event.offsetY + paddingTop + borderTop, }; }; onDragStart(event: DragEvent) { this.lastDropEvent = null; this.currentDragEffectMsg = ''; this.currentDraggableEvent = event; this.snackBarService.dismiss(); this.snackBarService.open('Drag started!', undefined, { duration: 2000 }); } onDragged($event: DragEvent, effect: string) { this.currentDragEffectMsg = `Drag ended with effect "${effect}"!`; } onDragEnd(event: DragEvent) { this.currentDraggableEvent = event; this.snackBarService.dismiss(); this.snackBarService.open( this.currentDragEffectMsg || `Drag ended!`, undefined, { duration: 2000 } ); } onDrop(event: DndDropEvent) { this.lastDropEvent = event; } } ================================================ FILE: projects/demo/src/app/tree/tree.component.html ================================================
@for (item of list; track item) {
{{ item.content }} @if (item.children) { }
}
{{ draggableList | json }}
================================================ FILE: projects/demo/src/app/tree/tree.component.scss ================================================ .dndDraggingSource { opacity: 0.5; transform: scale(0.98); pointer-events: none; & > * { pointer-events: none; } } ================================================ FILE: projects/demo/src/app/tree/tree.component.ts ================================================ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { DndDraggableDirective, DndDropEvent, DndDropzoneDirective, DndPlaceholderRefDirective, DropEffect, } from 'ngx-drag-drop'; interface DraggableItem { content: string; children: DraggableItem[]; } @Component({ selector: 'dnd-tree', standalone: true, imports: [ CommonModule, DndDropzoneDirective, DndPlaceholderRefDirective, MatIconModule, MatListModule, DndDraggableDirective, ], templateUrl: './tree.component.html', styleUrls: ['./tree.component.scss'], }) export default class TreeComponent { draggableList: DraggableItem[] = [ { content: 'Demo 1', children: [ { content: 'Nested 1', children: [], }, { content: 'Nested 2', children: [], }, { content: 'Nested 3', children: [], }, ], }, { content: 'Demo 2', children: [], }, { content: 'Demo 3', children: [], }, { content: 'Demo 4', children: [], }, { content: 'Demo 5', children: [], }, { content: 'Demo 6', children: [], }, { content: 'Demo 7', children: [], }, { content: 'Demo 8', children: [], }, { content: 'Demo 9', children: [], }, { content: 'Demo 10', children: [ { content: 'Nested 1', children: [], }, { content: 'Nested 2', children: [], }, { content: 'Nested 3', children: [], }, ], }, ]; onDragged(item: any, list: any[], effect: DropEffect) { if (effect === 'move') { const index = list.indexOf(item); list.splice(index, 1); } } onDrop(event: DndDropEvent, list?: any[]) { console.log('onDrop', event, list); if (list && (event.dropEffect === 'copy' || event.dropEffect === 'move')) { let index = event.index; if (typeof index === 'undefined') { index = list.length; } list.splice(index, 0, event.data); } } } ================================================ FILE: projects/demo/src/app/typed/typed.component.html ================================================
Fruits
@for (fruit of fruits; track trackByFruit(i, fruit); let i = $index) { {{ fruit.type }} {{ fruit.id }} }
Apples
@for (apple of apples; track trackByFruit(i, apple); let i = $index) { {{ apple.type }} {{ apple.id }} }
Bananas
@for ( banana of bananas; track trackByFruit(i, banana); let i = $index ) { {{ banana.type }} {{ banana.id }} }
================================================ FILE: projects/demo/src/app/typed/typed.component.scss ================================================ :host { display: block; box-sizing: border-box; } pre { text-align: center; padding: 8px; } .mat-mdc-list-item { margin: 2px 0; border: 1px solid gray; } .dndList { transition: all 300ms ease; padding: 8px; overflow-y: auto; &.dndDragover { padding-top: 12px; padding-bottom: 12px; border-color: green; } } .dndDragging { border: 1px solid green; } .dndDraggingSource { display: none; } .dndPlaceholder { min-height: 48px; border: 1px dashed green; background-color: rgba(0, 0, 0, 0.1); } pre { white-space: pre-wrap; word-wrap: break-word; } ================================================ FILE: projects/demo/src/app/typed/typed.component.ts ================================================ import { Component } from '@angular/core'; import { MatListModule } from '@angular/material/list'; import { DndDraggableDirective, DndDropEvent, DndDropzoneDirective, DndPlaceholderRefDirective, } from 'ngx-drag-drop'; type Apple = 'apple'; type Banana = 'banana'; type FruitType = Apple | Banana; interface Fruit { id: number; type: FruitType; } let id = 0; function createFruit(type: FruitType) { return { id: id++, type: type, }; } function range(start: number, end: number) { return Array.from({ length: end - start + 1 }, (_, i) => i); } @Component({ selector: 'dnd-typed', templateUrl: './typed.component.html', styleUrls: ['./typed.component.scss'], standalone: true, imports: [ MatListModule, DndDropzoneDirective, DndPlaceholderRefDirective, DndDraggableDirective, ], }) export default class TypedComponent { public fruits: Fruit[] = range(0, 100).map(_ => { const randomFruitType: FruitType = Math.random() < 0.5 ? 'apple' : 'banana'; return createFruit(randomFruitType); }); public apples: Fruit[] = range(0, 12).map(_ => { return createFruit('apple'); }); public bananas: Fruit[] = range(0, 10).map(_ => { return createFruit('banana'); }); trackByFruit(index: number, fruit: Fruit) { return fruit; } onDragged(index: number, fruit: Fruit, list: Fruit[]) { const removeIndex = list.indexOf(fruit); console.log( `onDragged ngFor-index=${index}, item=${fruit}, removeIndex=${removeIndex}, list=${list.length}` ); list.splice(removeIndex, 1); } onDrop(event: DndDropEvent, list: Fruit[]) { console.log('onDrop', event, list.length); let index = event.index; if (typeof index === 'undefined') { index = list.length; } list.splice(index, 0, event.data); } } ================================================ FILE: projects/demo/src/assets/.gitkeep ================================================ ================================================ FILE: projects/demo/src/dragdroptouch.d.ts ================================================ declare module '@dragdroptouch/drag-drop-touch' { export function enableDragDropTouch( dragRoot?: Element, dropRoot?: Element, options?: Record ): void; } ================================================ FILE: projects/demo/src/environments/environment.prod.ts ================================================ export const environment = { production: true, }; ================================================ FILE: projects/demo/src/environments/environment.ts ================================================ // This file can be replaced during build by using the `fileReplacements` array. // `ng build` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. export const environment = { production: false, }; /* * For easier debugging in development mode, you can import the following file * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. * * This import should be commented out in production mode because it will have a negative impact * on performance if an error is thrown. */ // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. ================================================ FILE: projects/demo/src/index.html ================================================ NgxDragDrop ================================================ FILE: projects/demo/src/main.ts ================================================ import { enableProdMode, provideZoneChangeDetection } from '@angular/core'; import { platformBrowser } from '@angular/platform-browser'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowser() .bootstrapModule(AppModule, { applicationProviders: [provideZoneChangeDetection()], }) .catch(err => console.error(err)); ================================================ FILE: projects/demo/src/polyfills.ts ================================================ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. * * This file is divided into 2 sections: * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. * 2. Application imports. Files imported after ZoneJS that should be loaded before your main * file. * * The current setup is for so-called "evergreen" browsers; the last versions of browsers that * automatically update themselves. This includes recent versions of Safari, Chrome (including * Opera), Edge on the desktop, and iOS and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** * BROWSER POLYFILLS */ /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags * because those flags need to be set before `zone.js` being loaded, and webpack * will put import in the top of bundle, so user need to create a separate file * in this directory (for example: zone-flags.ts), and put the following flags * into that file, and then add the following code before importing zone.js. * import './zone-flags'; * * The flags allowed in zone-flags.ts are listed here. * * The following flags will work for all browsers. * * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames * * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js * with the following flag, it will bypass `zone.js` patch for IE/Edge * * (window as any).__Zone_enable_cross_context_check = true; * */ /*************************************************************************************************** * APPLICATION IMPORTS */ import { enableDragDropTouch } from '@dragdroptouch/drag-drop-touch'; /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js'; // Included with Angular CLI. enableDragDropTouch(); ================================================ FILE: projects/demo/src/styles.scss ================================================ @import 'bootstrap/scss/bootstrap.scss'; .drag-handle { border: 1px solid #ddd; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; // Important because the material design would overwrite it in the mat-list width: 28px !important; height: 28px !important; } /* You can add global styles to this file, and also import other style files */ html, body { margin: 0; height: 100%; width: 100%; min-height: 100vh; display: flex; flex: 1; background: #fafafa; } .layout-padding { padding: 8px; } .layout-padding > * { margin: 8px; } .scrollable { -webkit-overflow-scrolling: touch; overflow-x: auto; overflow-y: auto; } [dndHandle], [draggable='true']:not(:has([dndHandle])) { cursor: pointer; } [draggable].dndDraggableDisabled { cursor: not-allowed; opacity: 0.7; } [dndDropzone] { // border: 1px red solid; min-width: 200px; min-height: 200px; } [dndDropzone].dndDropzoneDisabled { cursor: no-drop; opacity: 0.7; border-color: gray; } [dndDropzone].dndDragover { border-color: green; } html, body { height: 100%; } body { margin: 0; font-family: Roboto, 'Helvetica Neue', sans-serif; } ================================================ FILE: projects/demo/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/app", "types": [] }, "files": ["src/main.ts", "src/polyfills.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: projects/demo/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec" }, "files": ["src/test.ts", "src/polyfills.ts"], "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] } ================================================ FILE: projects/dnd/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: projects/dnd/README.md ================================================ [![npm](https://img.shields.io/npm/v/ngx-drag-drop.svg)](https://www.npmjs.com/package/ngx-drag-drop) [![npm (next)](https://img.shields.io/npm/v/ngx-drag-drop/next.svg)](https://www.npmjs.com/package/ngx-drag-drop) [![NpmLicense](https://img.shields.io/npm/l/ngx-drag-drop.svg)](https://www.npmjs.com/package/ngx-drag-drop) [![GitHub issues](https://img.shields.io/github/issues/ChristofFritz/ngx-drag-drop.svg)](https://github.com/ChristofFritz/ngx-drag-drop/issues) [![Twitter](https://img.shields.io/twitter/url/https/github.com/ChristofFritz/ngx-drag-drop.svg?style=social)](https://twitter.com/intent/tweet?text=Angular%20drag%20and%20drop%20with%20ease:&url=https://github.com/ChristofFritz/ngx-drag-drop) # NgxDragDrop [_Demo_](https://christoffritz.github.io/ngx-drag-drop/) / [_StackBlitz Issue Template_](https://stackblitz.com/edit/ngx-drag-drop-issue-template) ```sh npm install ngx-drag-drop # or pnpm add ngx-drag-drop ``` **Angular directives for declarative drag and drop using the HTML5 Drag-And-Drop API** - sortable lists by using placeholder element (vertical and horizontal) - nestable - dropzones optionally support external/native draggables (img, txt, file) - conditional drag/drop - typed drag/drop - utilize [EffectAllowed](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/effectAllowed) - custom CSS classes - touch support by using a [polyfill](#touch-support) - [AOT](https://angular.io/guide/aot-compiler) compatible Port of [angular-drag-drop-lists](https://github.com/marceljuenemann/angular-drag-and-drop-lists) but without the lists :wink: This has `dropzones` though :+1: The idea is that the directive does not handle lists internally so the `dndDropzone` can be general purpose. ## Angular Version Compatibility Starting with v13, the library major version matches the Angular major version. | Angular | ngx-drag-drop | | ------- | ------------- | | 21.x | 21.x | | 20.x | 20.x | | 19.x | 19.x | | 18.x | 18.x | | 17.x | 17.x | | 16.x | 16.x | | 15.x | 15.x | | 14.x | 14.x | | 13.x | 13.x | For older Angular versions (v4–v12), use ngx-drag-drop v2.x. ## Usage `app.component.html` ```HTML
HANDLE
draggable ({{draggable.effectAllowed}}) DISABLED
DRAG_IMAGE
dropzone
placeholder
``` `app.component` ```JS import {Component} from '@angular/core'; import {DndDropEvent} from 'ngx-drag-drop'; @Component() export class AppComponent { draggable = { // note that data is handled with JSON.stringify/JSON.parse // only set simple data or POJO's as methods will be lost data: "myDragData", effectAllowed: "all", disable: false, handle: false }; onDragStart(event: DragEvent) { console.log("drag started", JSON.stringify(event, null, 2)); } onDragEnd(event: DragEvent) { console.log("drag ended", JSON.stringify(event, null, 2)); } onDraggableCopied(event: DragEvent) { console.log("draggable copied", JSON.stringify(event, null, 2)); } onDraggableLinked(event: DragEvent) { console.log("draggable linked", JSON.stringify(event, null, 2)); } onDraggableMoved(event: DragEvent) { console.log("draggable moved", JSON.stringify(event, null, 2)); } onDragCanceled(event: DragEvent) { console.log("drag cancelled", JSON.stringify(event, null, 2)); } onDragover(event: DragEvent) { console.log("dragover", JSON.stringify(event, null, 2)); } onDrop(event: DndDropEvent) { console.log("dropped", JSON.stringify(event, null, 2)); } } ``` `app.module` ```JS import {BrowserModule} from '@angular/platform-browser'; import {NgModule} from '@angular/core'; import {DndModule} from 'ngx-drag-drop'; import {AppComponent} from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, DndModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } ``` ## API ```TS // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/dropEffect export type DropEffect = "move" | "copy" | "link" | "none"; // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/effectAllowed export type EffectAllowed = DropEffect | "copyMove" | "copyLink" | "linkMove" | "all"; ``` ```TS export type DndDragImageOffsetFunction = ( event:DragEvent, dragImage:Element ) => { x:number, y:number }; @Directive( { selector: "[dndDraggable]" } ) export declare class DndDraggableDirective { // the data attached to the drag dndDraggable: any; // the allowed drop effect dndEffectAllowed: EffectAllowed; // optionally set the type of dragged data to restrict dropping on compatible dropzones dndType?: string; // conditionally disable the draggability dndDisableIf: boolean; dndDisableDragIf: boolean; // set a custom class that is applied while dragging dndDraggingClass: string = "dndDragging"; // set a custom class that is applied to only the src element while dragging dndDraggingSourceClass: string = "dndDraggingSource"; // set the class that is applied when draggable is disabled by [dndDisableIf] dndDraggableDisabledClass = "dndDraggableDisabled"; // enables to set a function for calculating custom dragimage offset dndDragImageOffsetFunction:DndDragImageOffsetFunction = calculateDragImageOffset; // emits on drag start readonly dndStart: EventEmitter; // emits on drag readonly dndDrag: EventEmitter; // emits on drag end readonly dndEnd: EventEmitter; // emits when the dragged item has been dropped with effect "move" readonly dndMoved: EventEmitter; // emits when the dragged item has been dropped with effect "copy" readonly dndCopied: EventEmitter; // emits when the dragged item has been dropped with effect "link" readonly dndLinked: EventEmitter; // emits when the drag is canceled readonly dndCanceled: EventEmitter; } ``` ```TS export interface DndDropEvent { // the original drag event event: DragEvent; // the actual drop effect dropEffect: DropEffect; // true if the drag did not origin from a [dndDraggable] isExternal:boolean; // the data set on the [dndDraggable] that started the drag // for external drags use the event property which contains the original drop event as this will be undefined data?: any; // the index where the draggable was dropped in a dropzone // set only when using a placeholder index?: number; // if the dndType input on dndDraggable was set // it will be transported here type?: any; } @Directive( { selector: "[dndDropzone]" } ) export declare class DndDropzoneDirective { // optionally restrict the allowed types dndDropzone?: string[]; // set the allowed drop effect dndEffectAllowed: EffectAllowed; // conditionally disable the dropzone dndDisableIf: boolean; dndDisableDropIf: boolean; // if draggables that are not [dndDraggable] are allowed to be dropped // set to true if dragged text, images or files should be handled dndAllowExternal: boolean; // if its a horizontal list this influences how the placeholder position // is calculated dndHorizontal: boolean; // set the class applied to the dropzone // when a draggable is dragged over it dndDragoverClass: string = "dndDragover"; // set the class applied to the dropzone // when the dropzone is disabled by [dndDisableIf] dndDropzoneDisabledClass = "dndDropzoneDisabled"; // emits when a draggable is dragged over the dropzone readonly dndDragover: EventEmitter; // emits on successful drop readonly dndDrop: EventEmitter; } ``` ## Touch support Install the `mobile-drag-drop` module available on npm. Add the following lines to your js code ```JS import { polyfill } from 'mobile-drag-drop'; // optional import of scroll behaviour import { scrollBehaviourDragImageTranslateOverride } from "mobile-drag-drop/scroll-behaviour"; polyfill( { // use this to make use of the scroll behaviour dragImageTranslateOverride: scrollBehaviourDragImageTranslateOverride } ); // workaround to make scroll prevent work in iOS Safari > 10 try { window.addEventListener( "touchmove", function() { }, { passive: false } ); } catch(e){} ``` For more info on the polyfill check it out on GitHub https://github.com/timruffles/mobile-drag-drop ## Known issues ### Firefox - Beware that Firefox does not support dragging on `