Full Code of reppners/ngx-drag-drop for AI

master b888494da3a0 cached
89 files
147.4 KB
41.6k tokens
133 symbols
1 requests
Download .txt
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
<!--a draggable element-->
<div [dndDraggable]="draggable.data"
     [dndEffectAllowed]="draggable.effectAllowed"
     [dndDisableIf]="draggable.disable"
     (dndStart)="onDragStart($event)"
     (dndCopied)="onDraggableCopied($event)"
     (dndLinked)="onDraggableLinked($event)"
     (dndMoved)="onDraggableMoved($event)"
     (dndCanceled)="onDragCanceled($event)"
     (dndEnd)="onDragEnd($event)">

    <!--if [dndHandle] is used inside dndDraggable drag can only start from the handle-->
    <div *ngIf="draggable.handle"
         dndHandle>HANDLE
    </div>

    draggable ({{draggable.effectAllowed}}) <span [hidden]="!draggable.disable">DISABLED</span>

    <!--optionally select a child element as drag image-->
    <div dndDragImageRef>DRAG_IMAGE</div>

</div>

<!--a dropzone-->
<!--to allow dropping content that is not [dndDraggable] set dndAllowExternal to true-->
<section dndDropzone
         (dndDragover)="onDragover($event)"
         (dndDrop)="onDrop($event)">

    dropzone

    <!--optional placeholder element for dropzone-->
    <!--will be removed from DOM on init-->
    <div style="border: 1px orangered solid; border-radius: 5px; padding: 15px;"
         dndPlaceholderRef>
        placeholder
    </div>

</section>
```

`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<DragEvent>;

    // emits on drag
    readonly dndDrag: EventEmitter<DragEvent>;

    // emits on drag end
    readonly dndEnd: EventEmitter<DragEvent>;

    // emits when the dragged item has been dropped with effect "move"
    readonly dndMoved: EventEmitter<DragEvent>;

    // emits when the dragged item has been dropped with effect "copy"
    readonly dndCopied: EventEmitter<DragEvent>;

    // emits when the dragged item has been dropped with effect "link"
    readonly dndLinked: EventEmitter<DragEvent>;

    // emits when the drag is canceled
    readonly dndCanceled: EventEmitter<DragEvent>;
}
```

```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<DragEvent>;

    // emits on successful drop
    readonly dndDrop: EventEmitter<DndDropEvent>;
}
```

## 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 `<button>` elements.
  - `<button [dndDraggable]>` and `<button [dndHandler]>` won't work.
  - See https://bugzilla.mozilla.org/show_bug.cgi?id=568313

## Why?

HTML Drag-And-Drop API implementations are not behaving the same way across browsers.

The directives contained in this module enable declarative drag and drop that "just works" across browsers in a consistent way.

Credits go to the author and contributors of [angular-drag-drop-lists](https://github.com/marceljuenemann/angular-drag-and-drop-lists).

## Maintenance

This project was generated with [Angular CLI](https://github.com/angular/angular-cli).

See https://angular.io/guide/creating-libraries

#### Edit Library

- run `pnpm run watch:lib` for hacking on library

#### Release Library

- assure correct version is set in `projects/dnd/package.json`
- build library with `pnpm run build:lib`
- publish library with `pnpm run publish:stable` (use `pnpm run publish:next` for pre-releases)

#### Edit Docs

- initially and on lib changes run `pnpm run build:lib` to current version of lib available to the demo
- run `pnpm run start:docs`

#### Release Docs

The demo site is automatically deployed to GitHub Pages on every push to `master` via GitHub Actions.

---

<p align="center">
Made with :heart: & 
<a href="https://www.jetbrains.com/?from=ngx-drag-drop">
  <img align="center" alt="jetbrains" src="jetbrains.svg" width="120px">
</a>
& :coffee:
</p>


================================================
FILE: angular.json
================================================
{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "cli": {
    "analytics": false
  },
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "dnd": {
      "projectType": "library",
      "root": "projects/dnd",
      "sourceRoot": "projects/dnd/src",
      "prefix": "dnd",
      "architect": {
        "build": {
          "builder": "@angular/build:ng-packagr",
          "options": {
            "project": "projects/dnd/ng-package.json"
          },
          "configurations": {
            "production": {
              "tsConfig": "projects/dnd/tsconfig.lib.prod.json"
            },
            "development": {
              "tsConfig": "projects/dnd/tsconfig.lib.json"
            }
          },
          "defaultConfiguration": "production"
        }
      }
    },
    "demo": {
      "projectType": "application",
      "schematics": {
        "@schematics/angular:component": {
          "style": "scss"
        },
        "@schematics/angular:application": {
          "strict": true
        }
      },
      "root": "projects/demo",
      "sourceRoot": "projects/demo/src",
      "prefix": "dnd",
      "architect": {
        "build": {
          "builder": "@angular/build:application",
          "options": {
            "outputPath": {
              "base": "docs",
              "browser": ""
            },
            "index": "projects/demo/src/index.html",
            "polyfills": ["projects/demo/src/polyfills.ts"],
            "tsConfig": "projects/demo/tsconfig.app.json",
            "inlineStyleLanguage": "scss",
            "assets": [
              "projects/demo/src/favicon.ico",
              "projects/demo/src/assets"
            ],
            "styles": [
              "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
              "projects/demo/src/styles.scss"
            ],
            "scripts": [],
            "browser": "projects/demo/src/main.ts"
          },
          "configurations": {
            "production": {
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "2mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kb",
                  "maximumError": "4kb"
                }
              ],
              "fileReplacements": [
                {
                  "replace": "projects/demo/src/environments/environment.ts",
                  "with": "projects/demo/src/environments/environment.prod.ts"
                }
              ],
              "outputHashing": "all"
            },
            "development": {
              "optimization": false,
              "extractLicenses": false,
              "sourceMap": true,
              "namedChunks": true
            }
          },
          "defaultConfiguration": "production"
        },
        "serve": {
          "builder": "@angular/build:dev-server",
          "configurations": {
            "production": {
              "buildTarget": "demo:build:production"
            },
            "development": {
              "buildTarget": "demo:build:development"
            }
          },
          "defaultConfiguration": "development"
        },
        "extract-i18n": {
          "builder": "@angular/build:extract-i18n",
          "options": {
            "buildTarget": "demo:build"
          }
        }
      }
    }
  },
  "schematics": {
    "@schematics/angular:component": {
      "type": "component"
    },
    "@schematics/angular:directive": {
      "type": "directive"
    },
    "@schematics/angular:service": {
      "type": "service"
    },
    "@schematics/angular:guard": {
      "typeSeparator": "."
    },
    "@schematics/angular:interceptor": {
      "typeSeparator": "."
    },
    "@schematics/angular:module": {
      "typeSeparator": "."
    },
    "@schematics/angular:pipe": {
      "typeSeparator": "."
    },
    "@schematics/angular:resolver": {
      "typeSeparator": "."
    }
  }
}


================================================
FILE: package.json
================================================
{
  "name": "ngx-drag-drop-workspace",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "pnpm run build:lib && pnpm run start:docs",
    "start:docs": "ng serve demo",
    "build:lib": "ng build dnd",
    "watch:lib": "ng build dnd --watch --configuration development",
    "build:docs": "ng build demo --configuration production --base-href /ngx-drag-drop/ && cp docs/index.html docs/404.html",
    "watch:docs": "ng build demo --watch --configuration development",
    "publish:stable": "pnpm publish ./dist/ngx-drag-drop",
    "publish:next": "pnpm publish ./dist/ngx-drag-drop --tag next",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "prettier": "pnpm exec prettier --write ."
  },
  "private": true,
  "packageManager": "pnpm@10.33.0",
  "dependencies": {
    "@angular/animations": "^21.2.6",
    "@angular/cdk": "^21.2.4",
    "@angular/common": "^21.2.6",
    "@angular/compiler": "^21.2.6",
    "@angular/core": "^21.2.6",
    "@angular/forms": "^21.2.6",
    "@angular/material": "^21.2.4",
    "@angular/platform-browser": "^21.2.6",
    "@angular/platform-browser-dynamic": "^21.2.6",
    "@angular/router": "^21.2.6",
    "@dragdroptouch/drag-drop-touch": "^2.0.3",
    "bootstrap": "^5.3.8",
    "rxjs": "~7.8.2",
    "tslib": "^2.3.0",
    "zone.js": "~0.16.1"
  },
  "devDependencies": {
    "@analogjs/vite-plugin-angular": "2.4.8",
    "@angular/build": "21.2.7",
    "@angular/cli": "21.2.7",
    "@angular/compiler-cli": "21.2.9",
    "@types/node": "24.12.2",
    "@vitest/coverage-v8": "4.1.4",
    "jsdom": "29.0.2",
    "ng-packagr": "21.2.2",
    "prettier": "3.8.3",
    "typescript": "6.0.2",
    "vitest": "4.1.4"
  }
}


================================================
FILE: pnpm-workspace.yaml
================================================
allowBuilds:
  '@parcel/watcher': true
  esbuild: true
  lmdb: true
  msgpackr-extract: true


================================================
FILE: projects/demo/.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/demo/src/app/app.component.html
================================================
<mat-toolbar class="mat-elevation-z4 flex-shrink-0" color="primary">
  <span>{{ title }}</span>

  <span class="flex-grow-1"></span>

  <button [matMenuTriggerFor]="issueMenu" mat-button>
    <mat-icon fontIcon="bug_report"></mat-icon>
    Issue Demos
  </button>
  <mat-menu #issueMenu="matMenu">
    @for (demo of issueDemos; track demo.issue) {
      <button (click)="onIssueDemoClick(demo.issue)" mat-menu-item>
        {{ demo.label }}
      </button>
    }
  </mat-menu>

  <a href="https://github.com/ChristofFritz/ngx-drag-drop" mat-icon-button>
    <mat-icon svgIcon="github"></mat-icon>
  </a>
</mat-toolbar>

@if (activeTab$ | async; as activeTab) {
  <nav [tabPanel]="tabPanel" mat-tab-nav-bar>
    @for (tab of tabs; track tab) {
      <a
        [active]="activeTab === tab"
        (click)="onTabLinkClick(tab)"
        mat-tab-link>
        {{ tab }}
      </a>
    }
  </nav>
}

<mat-tab-nav-panel #tabPanel class="flex-grow-1 pt-3 overflow-auto">
  <router-outlet></router-outlet>
</mat-tab-nav-panel>

<mat-divider></mat-divider>

<dnd-demo-link [name]="activeTab$ | async" style="margin: 8px"> </dnd-demo-link>


================================================
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<string>;

  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
================================================
<a [href]="url" color="accent" mat-raised-button>go to sources</a>


================================================
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
================================================
<mat-icon dndHandle fontIcon="drag_handle"></mat-icon>


================================================
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
================================================
<div dndDragImageRef>
  <ng-content></ng-content>
</div>


================================================
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
================================================
<h3>Issue #195: dropEffect ignores dropzone's dndEffectAllowed</h3>
<p>
  Draggable has <code>dndEffectAllowed="all"</code>. Dropzone has
  <code>dndEffectAllowed="link"</code>. On drop, the
  <code>dropEffect</code> should be <code>"link"</code>, not
  <code>"move"</code>.
</p>

<div class="demo">
  <div
    [dndDraggable]="'test-data'"
    dndEffectAllowed="all"
    (dndMoved)="dragEffect = 'move'"
    (dndCopied)="dragEffect = 'copy'"
    (dndLinked)="dragEffect = 'link'"
    (dndCanceled)="dragEffect = 'none'"
    class="draggable">
    Drag me (effectAllowed: all)
  </div>

  <div
    (dndDrop)="onDrop($event)"
    class="dropzone"
    dndDropzone
    dndEffectAllowed="link">
    <div class="placeholder" dndPlaceholderRef></div>
    Drop here (effectAllowed: link)
  </div>
</div>

@if (lastDropEvent) {
  <div class="result">
    <div>
      <strong
        >Dropzone received dropEffect: {{ lastDropEvent.dropEffect }}</strong
      >
      <span
        [class.pass]="lastDropEvent.dropEffect === 'link'"
        [class.fail]="lastDropEvent.dropEffect !== 'link'">
        {{
          lastDropEvent.dropEffect === 'link'
            ? 'PASS'
            : 'FAIL — expected "link"'
        }}
      </span>
    </div>
    <div>
      <strong>Draggable fired effect: {{ dragEffect }}</strong>
      <span
        [class.pass]="dragEffect === 'link'"
        [class.fail]="dragEffect !== 'link'">
        {{ dragEffect === 'link' ? 'PASS' : 'FAIL — expected "link"' }}
      </span>
    </div>
  </div>
}


================================================
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
================================================
<div class="container-fluid">
  <div class="row">
    <div class="col">
      <pre>dndEffectAllowed="copyMove"</pre>

      <mat-list
        [class.horizontal]="layout.dndHorizontal"
        [dndHorizontal]="layout.dndHorizontal"
        (dndDrop)="onDrop($event, draggableListLeft)"
        class="dndList gap-1 d-flex flex-column bg-light rounded-1 shadow-sm"
        dndDropzone
        dndEffectAllowed="copyMove">
        <mat-list-item
          class="dndPlaceholder border rounded-1 bg-opacity-25"
          dndPlaceholderRef>
        </mat-list-item>

        @for (item of draggableListLeft; track item) {
          <mat-list-item
            [dndDisableIf]="item.disable"
            [dndDraggable]="item"
            [dndEffectAllowed]="item.effectAllowed"
            (dndCanceled)="onDragged(item, draggableListLeft, 'none')"
            (dndCopied)="onDragged(item, draggableListLeft, 'copy')"
            (dndEnd)="onDragEnd($event)"
            (dndLinked)="onDragged(item, draggableListLeft, 'link')"
            (dndMoved)="onDragged(item, draggableListLeft, 'move')"
            (dndStart)="onDragStart($event)"
            class="border rounded-1 bg-white">
            @if (item.handle) {
              <div
                class="drag-handle align-self-center mx-3 my-0"
                dndHandle
                matListItemIcon>
                <mat-icon fontIcon="drag_handle"></mat-icon>
              </div>
            }
            <span matListItemTitle>{{ item.content }}</span>
            <span matListItemLine>effectAllowed: {{ item.effectAllowed }}</span>
          </mat-list-item>
        }
      </mat-list>
    </div>

    <div class="col d-flex flex-column">
      <pre>dndEffectAllowed="copyMove"</pre>

      <mat-list
        [class.horizontal]="layout.dndHorizontal"
        [dndHorizontal]="layout.dndHorizontal"
        (dndDrop)="onDrop($event, draggableListRight)"
        class="dndList gap-1 flex-grow-1 d-flex flex-column bg-light rounded-1 shadow-sm"
        dndDropzone
        dndEffectAllowed="copyMove">
        <mat-list-item
          class="dndPlaceholder border rounded-1 bg-white bg-opacity-25"
          dndPlaceholderRef>
        </mat-list-item>
        @for (item of draggableListRight; track item) {
          <mat-list-item
            [dndDisableIf]="item.disable"
            [dndDraggable]="item"
            [dndEffectAllowed]="item.effectAllowed"
            (dndCanceled)="onDragged(item, draggableListRight, 'none')"
            (dndCopied)="onDragged(item, draggableListRight, 'copy')"
            (dndEnd)="onDragEnd($event)"
            (dndLinked)="onDragged(item, draggableListRight, 'link')"
            (dndMoved)="onDragged(item, draggableListRight, 'move')"
            (dndStart)="onDragStart($event)"
            class="border rounded-1 bg-white">
            @if (item.handle) {
              <div
                class="drag-handle align-self-center mx-3 my-0"
                dndHandle
                matListItemIcon>
                <mat-icon fontIcon="drag_handle"></mat-icon>
              </div>
            }
            <span matListItemTitle>{{ item.content }}</span>
            <span matListItemLine>effectAllowed: {{ item.effectAllowed }}</span>
          </mat-list-item>
        }
      </mat-list>
    </div>

    <div class="col d-flex flex-column">
      <pre>trash (dndEffectAllowed="move")</pre>

      <mat-list
        [dndHorizontal]="layout.dndHorizontal"
        (dndDrop)="onDrop($event)"
        class="dndList gap-1 flex-grow-1 d-flex flex-column bg-light rounded-1 shadow-sm"
        dndDropzone
        dndEffectAllowed="move">
      </mat-list>
    </div>
  </div>
</div>

<!-- <div class="layout-padding"> -->
<!--   <mat-slide-toggle [checked]="horizontalLayoutActive" -->
<!--                     (change)="horizontalLayoutActive = $event.checked; setHorizontalLayout($event.checked)">horizontal -->
<!--   </mat-slide-toggle> -->
<!-- </div> -->


================================================
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
================================================
<div class="container-fluid">
  <div class="row">
    <div class="col">
      <mat-card appearance="raised">
        <mat-card-header>
          <mat-card-title>Draggable stuff</mat-card-title>
          <mat-card-subtitle>
            Drag the image or a text selection onto the dropzone and see what
            happens
          </mat-card-subtitle>
        </mat-card-header>
        <mat-card-content class="d-flex flex-column gap-3">
          <div>
            <img src="https://i.imgflip.com/1quv8v.jpg" />
          </div>

          <div>
            <a href="https://imgflip.com/i/1quv8v">Made with imgflip</a>
          </div>

          <a href="#">
            Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
            nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
            erat, sed diam voluptua. At vero eos et accusam et justo duo dolores
            et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est
            Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur
            sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore
            et dolore magna aliquyam erat, sed diam voluptua. At vero eos et
            accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren,
            no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum
            dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod
            tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
            voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
            Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum
            dolor sit amet.
          </a>
        </mat-card-content>
      </mat-card>
    </div>

    <div class="col">
      <mat-card appearance="raised">
        <mat-card-header>
          <mat-card-title>Dropzone</mat-card-title>
        </mat-card-header>
        <mat-card-content>
          <div
            [dndAllowExternal]="true"
            (dndDrop)="onDrop($event)"
            class="mt-2 p-2 border rounded"
            dndDropzone>
            <div>
              @if (lastDropEvent) {
                <pre>Event: {{ lastDropEvent | json }}</pre>
              }
              @if (lastDropTypes && lastDropTypes.length) {
                <pre>Types: {{ lastDropTypes | json }}</pre>
              }
              @if (lastDropFiles && lastDropFiles.length) {
                <pre>Files: {{ lastDropFiles | json }}</pre>
              }
              @if (lastDropItems && lastDropItems.length) {
                <pre>Items: {{ lastDropItems | json }}</pre>
              }
            </div>
          </div>
        </mat-card-content>
      </mat-card>
    </div>
  </div>
</div>


================================================
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<string>;
  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
================================================
<div class="container-fluid">
  <ng-template #recursiveList let-list>
    <mat-card appearance="raised" class="dndPlaceholder" dndPlaceholderRef>
    </mat-card>

    @for (item of list; track item) {
      <mat-card
        appearance="raised"
        [dndDisableIf]="!!item.disable"
        [dndDraggable]="item"
        (dndCanceled)="onDragged(item, list, 'none')"
        (dndCopied)="onDragged(item, list, 'copy')"
        (dndEnd)="onDragEnd($event)"
        (dndLinked)="onDragged(item, list, 'link')"
        (dndMoved)="onDragged(item, list, 'move')"
        (dndStart)="onDragStart($event)"
        dndEffectAllowed="move">
        <mat-card-header class="p-2">
          @if (item.handle) {
            <div class="drag-handle me-2 text-muted">
              <mat-icon dndHandle mat-list-icon>drag_handle </mat-icon>
            </div>
          }
          {{ item.content }}
        </mat-card-header>
        <mat-card-content
          class="d-flex align-items-start gap-2 flex-column p-2">
          @if (!!item.customDragImage) {
            <div dndDragImageRef>MY_CUSTOM_DRAG_IMAGE</div>
          }
          @if (item.children) {
            <div
              (dndDrop)="onDrop($event, item.children)"
              class="flex-column p-2 gap-2 rounded-2"
              dndDropzone>
              <ng-container
                *ngTemplateOutlet="
                  recursiveList;
                  context: { $implicit: item.children }
                "></ng-container>
            </div>
          }
        </mat-card-content>
      </mat-card>
    }
  </ng-template>

  <div
    (dndDrop)="onDrop($event, nestableList)"
    class="d-flex gap-3 p-3 rounded-2"
    dndDropzone>
    <ng-container
      *ngTemplateOutlet="
        recursiveList;
        context: { $implicit: nestableList }
      "></ng-container>
  </div>
</div>


================================================
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
================================================
<ng-template #recursiveList let-list>
  <div class="placeholder" dndPlaceholderRef></div>

  @for (item of list; track item) {
    <div
      [dndDraggable]="item"
      dndEffectAllowed="move"
      (dndMoved)="onDragged(item, list, 'move')"
      class="drag-item">
      <div class="drag-item-header">{{ item.content }}</div>
      @if (item.children) {
        <div
          (dndDrop)="onDrop($event, item.children)"
          class="nested-dropzone"
          dndDropzone>
          <ng-container
            *ngTemplateOutlet="
              recursiveList;
              context: { $implicit: item.children }
            "></ng-container>
        </div>
      }
    </div>
  }
</ng-template>

<div (dndDrop)="onDrop($event, nestableList)" class="root-dropzone" dndDropzone>
  <ng-container
    *ngTemplateOutlet="
      recursiveList;
      context: { $implicit: nestableList }
    "></ng-container>
</div>


================================================
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
================================================
<div class="container-fluid">
  <div class="row gap-3 gap-sm-0">
    <div class="d-flex flex-column col gap-2">
      @for (draggable of draggables; track draggable) {
        <mat-card
          appearance="raised"
          [dndDisableIf]="draggable.disable"
          [dndDraggable]="draggable.content"
          [dndEffectAllowed]="draggable.effectAllowed"
          (dndCanceled)="onDragged($event, 'none')"
          (dndCopied)="onDragged($event, 'copy')"
          (dndEnd)="onDragEnd($event)"
          (dndLinked)="onDragged($event, 'link')"
          (dndMoved)="onDragged($event, 'move')"
          (dndStart)="onDragStart($event)">
          <mat-card-content class="d-flex flex-column gap-2">
            @if (draggable.handle) {
              <div class="drag-handle" dndHandle>
                <mat-icon fontIcon="drag_handle" mat-list-icon> </mat-icon>
              </div>
            }
            draggable ({{ draggable.effectAllowed }})
            <span [hidden]="!draggable.handle"> only with handle</span>
            <span [hidden]="!draggable.disable"> DISABLED</span>
          </mat-card-content>
        </mat-card>
      }

      <mat-card
        appearance="raised"
        [dndDisableIf]="draggableWithDragImage.disable"
        [dndDraggable]="draggableWithDragImage"
        [dndEffectAllowed]="draggableWithDragImage.effectAllowed"
        (dndCanceled)="onDragged($event, 'none')"
        (dndCopied)="onDragged($event, 'copy')"
        (dndEnd)="onDragEnd($event)"
        (dndLinked)="onDragged($event, 'link')"
        (dndMoved)="onDragged($event, 'move')"
        (dndStart)="onDragStart($event)">
        <mat-card-content class="d-flex flex-column gap-2">
          @if (draggableWithDragImage.handle) {
            <div class="drag-handle">
              <mat-icon dndHandle fontIcon="drag_handle" mat-list-icon>
              </mat-icon>
            </div>
          }
          draggable ({{ draggableWithDragImage.effectAllowed }})
          <span [hidden]="!draggableWithDragImage.handle"
            >only with handle</span
          >
          <span [hidden]="!draggableWithDragImage.disable"> DISABLED</span>

          <div dndDragImageRef>MY_CUSTOM_DRAG_IMAGE</div>
        </mat-card-content>
      </mat-card>

      <mat-card
        appearance="raised"
        [dndDisableIf]="draggableWithDragImage.disable"
        [dndDraggable]="draggableWithDragImage"
        [dndEffectAllowed]="draggableWithDragImage.effectAllowed"
        (dndCanceled)="onDragged($event, 'none')"
        (dndCopied)="onDragged($event, 'copy')"
        (dndEnd)="onDragEnd($event)"
        (dndLinked)="onDragged($event, 'link')"
        (dndMoved)="onDragged($event, 'move')"
        (dndStart)="onDragStart($event)">
        <mat-card-content class="d-flex flex-column gap-2">
          <dnd-indirect-handle></dnd-indirect-handle>

          draggable ({{ draggableWithDragImage.effectAllowed }})

          <dnd-indirect-drag-image
            >I'm the drag image but pssst</dnd-indirect-drag-image
          >
        </mat-card-content>
      </mat-card>

      <mat-card
        appearance="raised"
        [dndDisableIf]="draggableWithDragImage.disable"
        [dndDragImageOffsetFunction]="dragImageOffsetRight"
        [dndDraggable]="draggableWithDragImage"
        [dndEffectAllowed]="draggableWithDragImage.effectAllowed"
        (dndCanceled)="onDragged($event, 'none')"
        (dndCopied)="onDragged($event, 'copy')"
        (dndEnd)="onDragEnd($event)"
        (dndLinked)="onDragged($event, 'link')"
        (dndMoved)="onDragged($event, 'move')"
        (dndStart)="onDragStart($event)">
        <mat-card-content class="d-flex flex-column gap-2">
          <div>draggable ({{ draggableWithDragImage.effectAllowed }})</div>

          <dnd-indirect-handle></dnd-indirect-handle>
        </mat-card-content>
      </mat-card>
    </div>

    <div class="col">
      <mat-card appearance="raised">
        <mat-card-header>
          <mat-card-title>Dropzone</mat-card-title>
          <mat-card-subtitle>
            <mat-slide-toggle
              [checked]="dropzoneEnabled"
              (change)="dropzoneEnabled = $event.checked">
              Enabled
            </mat-slide-toggle>
          </mat-card-subtitle>
        </mat-card-header>
        <mat-card-content>
          <section
            [dndDisableIf]="!dropzoneEnabled"
            (dndDrop)="onDrop($event)"
            class="my-dropzone mt-3 p-2 border rounded"
            dndDragoverClass="custom-drag-over"
            dndDropzone>
            @if (lastDropEvent) {
              <pre>{{ lastDropEvent | json }}</pre>
            }
          </section>
        </mat-card-content>
      </mat-card>
    </div>
  </div>
</div>


================================================
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
================================================
<div class="container-fluid">
  <ng-template #recursiveList let-list>
    <mat-list-item
      class="dndPlaceholder border bg-opacity-25 mb-1"
      dndPlaceholderRef />

    @for (item of list; track item) {
      <div class="">
        <mat-list-item
          [dndDraggable]="item"
          dndEffectAllowed="move"
          (dndMoved)="onDragged(item, list, 'move')"
          class="border bg-white ms-n3">
          <span matListItemTitle>{{ item.content }}</span>
        </mat-list-item>
        @if (item.children) {
          <mat-list
            (dndDrop)="onDrop($event, item.children)"
            class="d-flex flex-column bg-light pt-2 pb-0 ps-2"
            style="min-height: unset"
            dndDropzone
            dndEffectAllowed="move">
            <ng-container
              *ngTemplateOutlet="
                recursiveList;
                context: { $implicit: item.children }
              " />
          </mat-list>
        }
      </div>
    }
  </ng-template>

  <div class="row">
    <div class="col-12 offset-lg-2 col-lg-4">
      <mat-list
        (dndDrop)="onDrop($event, draggableList)"
        class="d-flex flex-column bg-light gap-1"
        dndDropzone
        dndEffectAllowed="move">
        <ng-container
          *ngTemplateOutlet="
            recursiveList;
            context: { $implicit: draggableList }
          " />
      </mat-list>
    </div>
    <div class="col-12 col-lg-4">
      <pre>{{ draggableList | json }}</pre>
    </div>
  </div>
</div>


================================================
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
================================================
<div class="container-fluid">
  <div class="row">
    <div class="col">
      <pre>Fruits</pre>
      <mat-list
        (dndDrop)="onDrop($event, fruits)"
        class="dndList gap-1 flex-grow-1 d-flex flex-column bg-light rounded-1 shadow-sm"
        dndDropzone
        dndEffectAllowed="move">
        <mat-list-item
          class="dndPlaceholder border rounded-1 bg-danger bg-gradient"
          dndPlaceholderRef>
        </mat-list-item>
        @for (fruit of fruits; track trackByFruit(i, fruit); let i = $index) {
          <mat-list-item
            [dndDraggable]="fruit"
            [dndType]="fruit.type"
            (dndMoved)="onDragged(i, fruit, fruits)"
            class="border rounded-1 bg-white"
            dndEffectAllowed="move">
            <span matListItemTitle>{{ fruit.type }} {{ fruit.id }}</span>
          </mat-list-item>
        }
      </mat-list>
    </div>

    <div class="col">
      <pre>Apples</pre>
      <mat-list
        [dndDropzone]="['apple']"
        (dndDrop)="onDrop($event, apples)"
        class="dndList gap-1 flex-grow-1 d-flex flex-column bg-light rounded-1 shadow-sm"
        dndEffectAllowed="move">
        <mat-list-item
          class="dndPlaceholder border rounded-1 bg-success bg-gradient"
          dndPlaceholderRef>
        </mat-list-item>
        @for (apple of apples; track trackByFruit(i, apple); let i = $index) {
          <mat-list-item
            [dndDraggable]="apple"
            [dndType]="apple.type"
            (dndMoved)="onDragged(i, apple, apples)"
            class="border rounded-1 bg-white"
            dndEffectAllowed="move">
            <span matListItemTitle>{{ apple.type }} {{ apple.id }}</span>
          </mat-list-item>
        }
      </mat-list>
    </div>

    <div class="col">
      <pre>Bananas</pre>
      <mat-list
        [dndDropzone]="['banana']"
        (dndDrop)="onDrop($event, bananas)"
        class="dndList gap-1 flex-grow-1 d-flex flex-column bg-light rounded-1 shadow-sm"
        dndEffectAllowed="move">
        <mat-list-item
          class="dndPlaceholder border rounded-1 bg-warning bg-gradient"
          dndPlaceholderRef>
        </mat-list-item>
        @for (
          banana of bananas;
          track trackByFruit(i, banana);
          let i = $index
        ) {
          <mat-list-item
            [dndDraggable]="banana"
            [dndType]="banana.type"
            (dndMoved)="onDragged(i, banana, bananas)"
            class="border rounded-1 bg-white"
            dndEffectAllowed="move">
            <span matListItemTitle>{{ banana.type }} {{ banana.id }}</span>
          </mat-list-item>
        }
      </mat-list>
    </div>
  </div>
</div>


================================================
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<string, unknown>
  ): 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
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>NgxDragDrop</title>
    <base href="/" />
    <meta content="width=device-width, initial-scale=1" name="viewport" />
    <link href="favicon.ico" rel="icon" type="image/x-icon" />
    <link href="https://fonts.gstatic.com" rel="preconnect" />
    <link
      href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
      rel="stylesheet" />
    <link
      href="https://fonts.googleapis.com/icon?family=Material+Icons"
      rel="stylesheet" />
  </head>
  <body class="mat-typography">
    <dnd-root></dnd-root>
  </body>
</html>


================================================
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
<!--a draggable element-->
<div
  [dndDraggable]="draggable.data"
  [dndEffectAllowed]="draggable.effectAllowed"
  [dndDisableIf]="draggable.disable"
  (dndStart)="onDragStart($event)"
  (dndCopied)="onDraggableCopied($event)"
  (dndLinked)="onDraggableLinked($event)"
  (dndMoved)="onDraggableMoved($event)"
  (dndCanceled)="onDragCanceled($event)"
  (dndEnd)="onDragEnd($event)">

  <!--if [dndHandle] is used inside dndDraggable drag can only start from the handle-->
  <div
    *ngIf="draggable.handle"
    dndHandle>HANDLE
  </div>

  draggable ({{draggable.effectAllowed}}) <span [hidden]="!draggable.disable">DISABLED</span>

  <!--optionally select a child element as drag image-->
  <div dndDragImageRef>DRAG_IMAGE</div>

</div>

<!--a dropzone-->
<!--to allow dropping content that is not [dndDraggable] set dndAllowExternal to true-->
<section
  dndDropzone
  (dndDragover)="onDragover($event)"
  (dndDrop)="onDrop($event)">

  dropzone

  <!--optional placeholder element for dropzone-->
  <!--will be removed from DOM on init-->
  <div
    style="border: 1px orangered solid; border-radius: 5px; padding: 15px;"
    dndPlaceholderRef>
    placeholder
  </div>

</section>
```

`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<DragEvent>;

    // emits on drag
    readonly dndDrag: EventEmitter<DragEvent>;

    // emits on drag end
    readonly dndEnd: EventEmitter<DragEvent>;

    // emits when the dragged item has been dropped with effect "move"
    readonly dndMoved: EventEmitter<DragEvent>;

    // emits when the dragged item has been dropped with effect "copy"
    readonly dndCopied: EventEmitter<DragEvent>;

    // emits when the dragged item has been dropped with effect "link"
    readonly dndLinked: EventEmitter<DragEvent>;

    // emits when the drag is canceled
    readonly dndCanceled: EventEmitter<DragEvent>;
}
```

```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<DragEvent>;

    // emits on successful drop
    readonly dndDrop: EventEmitter<DndDropEvent>;
}
```

## 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 `<button>` elements.
  - `<button [dndDraggable]>` and `<button [dndHandler]>` won't work.
  - See https://bugzilla.mozilla.org/show_bug.cgi?id=568313

## Why?

HTML Drag-And-Drop API implementations are not behaving the same way across browsers.

The directives contained in this module enable declarative drag and drop that "just works" across browsers in a consistent way.

Credits go to the author and contributors of [angular-drag-drop-lists](https://github.com/marceljuenemann/angular-drag-and-drop-lists).


================================================
FILE: projects/dnd/ng-package.json
================================================
{
  "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
  "dest": "../../dist/ngx-drag-drop",
  "lib": {
    "entryFile": "src/public-api.ts"
  }
}


================================================
FILE: projects/dnd/package.json
================================================
{
  "name": "ngx-drag-drop",
  "version": "21.0.6",
  "description": "Angular directives using the native HTML Drag And Drop API",
  "repository": {
    "type": "git",
    "url": "https://github.com/ChristofFritz/ngx-drag-drop.git"
  },
  "homepage": "https://christoffritz.github.io/ngx-drag-drop/",
  "author": {
    "name": "Christof Fritz",
    "email": "npm@christof.me"
  },
  "license": "BSD-3-Clause",
  "keywords": [
    "angular",
    "html",
    "drag",
    "drop",
    "dragdrop",
    "dragndrop",
    "dnd",
    "directive",
    "touch"
  ],
  "peerDependencies": {
    "@angular/common": ">=21.0.0",
    "@angular/core": ">=21.0.0"
  },
  "dependencies": {
    "tslib": "^2.3.0"
  }
}


================================================
FILE: projects/dnd/src/lib/dnd-draggable.directive.spec.ts
================================================
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { describe, it, expect, beforeEach } from 'vitest';
import {
  DndDraggableDirective,
  DndDragImageRefDirective,
} from './dnd-draggable.directive';
import { DndHandleDirective } from './dnd-handle.directive';
import { endDrag, dndState } from './dnd-state';

@Component({
  standalone: true,
  imports: [DndDraggableDirective],
  template: `<div [dndDraggable]="'testData'" [dndEffectAllowed]="'copyMove'">
    drag me
  </div>`,
})
class BasicDraggableHost {}

@Component({
  standalone: true,
  imports: [DndDraggableDirective],
  template: `<div [dndDraggable]="'data'">drag me</div>`,
})
class DisabledDraggableHost {}

@Component({
  standalone: true,
  imports: [DndDraggableDirective, DndHandleDirective],
  template: `
    <div [dndDraggable]="'data'">
      <div dndHandle>handle</div>
      content
    </div>
  `,
})
class HandleDraggableHost {}

@Component({
  standalone: true,
  imports: [DndDraggableDirective, DndDragImageRefDirective],
  template: `
    <div [dndDraggable]="'data'">
      <div dndDragImageRef>custom image</div>
      content
    </div>
  `,
})
class DragImageHost {}

describe('DndDraggableDirective', () => {
  let fixture: ComponentFixture<BasicDraggableHost>;
  let draggableEl: DebugElement;

  beforeEach(async () => {
    endDrag();
    await TestBed.configureTestingModule({
      imports: [BasicDraggableHost],
    }).compileComponents();

    fixture = TestBed.createComponent(BasicDraggableHost);
    fixture.detectChanges();
    draggableEl = fixture.debugElement.query(
      By.directive(DndDraggableDirective)
    );
  });

  it('should set draggable attribute to true', () => {
    expect(draggableEl.nativeElement.getAttribute('draggable')).toBe('true');
  });

  it('should have the directive instance', () => {
    const directive = draggableEl.injector.get(DndDraggableDirective);
    expect(directive).toBeTruthy();
    expect(directive.dndEffectAllowed).toBe('copyMove');
  });
});

describe('DndDraggableDirective - disabled', () => {
  let fixture: ComponentFixture<DisabledDraggableHost>;
  let directive: DndDraggableDirective;
  let draggableEl: DebugElement;

  beforeEach(async () => {
    endDrag();
    await TestBed.configureTestingModule({
      imports: [DisabledDraggableHost],
    }).compileComponents();

    fixture = TestBed.createComponent(DisabledDraggableHost);
    fixture.detectChanges();
    draggableEl = fixture.debugElement.query(
      By.directive(DndDraggableDirective)
    );
    directive = draggableEl.injector.get(DndDraggableDirective);
  });

  it('should set draggable to false when disabled', () => {
    directive.dndDisableIf = true;
    expect(directive.draggable).toBe(false);
    expect(
      draggableEl.nativeElement.classList.contains('dndDraggableDisabled')
    ).toBe(true);
  });

  it('should add disabled class when disabled', () => {
    directive.dndDisableIf = true;
    expect(
      draggableEl.nativeElement.classList.contains('dndDraggableDisabled')
    ).toBe(true);
  });

  it('should remove disabled class when re-enabled', () => {
    directive.dndDisableIf = true;
    directive.dndDisableIf = false;
    expect(directive.draggable).toBe(true);
    expect(
      draggableEl.nativeElement.classList.contains('dndDraggableDisabled')
    ).toBe(false);
  });
});

describe('DndDraggableDirective - handle', () => {
  let fixture: ComponentFixture<HandleDraggableHost>;

  beforeEach(async () => {
    endDrag();
    await TestBed.configureTestingModule({
      imports: [HandleDraggableHost],
    }).compileComponents();

    fixture = TestBed.createComponent(HandleDraggableHost);
    fixture.detectChanges();
  });

  it('should register the handle', () => {
    const draggableEl = fixture.debugElement.query(
      By.directive(DndDraggableDirective)
    );
    const directive = draggableEl.injector.get(DndDraggableDirective);
    // The handle registers itself on init — verify via the private field
    expect((directive as any).dndHandle).toBeTruthy();
  });
});

describe('DndDraggableDirective - drag image', () => {
  let fixture: ComponentFixture<DragImageHost>;

  beforeEach(async () => {
    endDrag();
    await TestBed.configureTestingModule({
      imports: [DragImageHost],
    }).compileComponents();

    fixture = TestBed.createComponent(DragImageHost);
    fixture.detectChanges();
  });

  it('should register the drag image element', () => {
    const draggableEl = fixture.debugElement.query(
      By.directive(DndDraggableDirective)
    );
    const directive = draggableEl.injector.get(DndDraggableDirective);
    expect((directive as any).dndDragImageElementRef).toBeTruthy();
  });
});


================================================
FILE: projects/dnd/src/lib/dnd-draggable.directive.ts
================================================
import {
  AfterViewInit,
  Directive,
  ElementRef,
  EventEmitter,
  forwardRef,
  HostBinding,
  HostListener,
  inject,
  Input,
  NgZone,
  OnDestroy,
  OnInit,
  Output,
  Renderer2,
} from '@angular/core';
import { DndHandleDirective } from './dnd-handle.directive';
import { dndState, endDrag, startDrag } from './dnd-state';
import { EffectAllowed } from './dnd-types';
import {
  calculateDragImageOffset,
  DndDragImageOffsetFunction,
  DndEvent,
  setDragData,
  setDragImage,
} from './dnd-utils';

@Directive({ selector: '[dndDragImageRef]', standalone: true })
export class DndDragImageRefDirective implements OnInit {
  dndDraggableDirective = inject(forwardRef(() => DndDraggableDirective));
  elementRef: ElementRef<HTMLElement> = inject(ElementRef);

  ngOnInit() {
    this.dndDraggableDirective.registerDragImage(this.elementRef);
  }
}

@Directive({ selector: '[dndDraggable]', standalone: true })
export class DndDraggableDirective implements AfterViewInit, OnDestroy {
  @Input() dndDraggable: any;
  @Input() dndEffectAllowed: EffectAllowed = 'copy';
  @Input() dndType?: string;
  @Input() dndDraggingClass = 'dndDragging';
  @Input() dndDraggingSourceClass = 'dndDraggingSource';
  @Input() dndDraggableDisabledClass = 'dndDraggableDisabled';
  @Input() dndDragImageOffsetFunction: DndDragImageOffsetFunction =
    calculateDragImageOffset;

  @Output() readonly dndStart: EventEmitter<DragEvent> =
    new EventEmitter<DragEvent>();
  @Output() readonly dndDrag: EventEmitter<DragEvent> =
    new EventEmitter<DragEvent>();
  @Output() readonly dndEnd: EventEmitter<DragEvent> =
    new EventEmitter<DragEvent>();
  @Output() readonly dndMoved: EventEmitter<DragEvent> =
    new EventEmitter<DragEvent>();
  @Output() readonly dndCopied: EventEmitter<DragEvent> =
    new EventEmitter<DragEvent>();
  @Output() readonly dndLinked: EventEmitter<DragEvent> =
    new EventEmitter<DragEvent>();
  @Output() readonly dndCanceled: EventEmitter<DragEvent> =
    new EventEmitter<DragEvent>();

  @HostBinding('attr.draggable') draggable = true;

  private dndHandle?: DndHandleDirective;
  private dndDragImageElementRef?: ElementRef;
  private dragImage: Element | undefined;
  private isDragStarted: boolean = false;

  private elementRef: ElementRef<HTMLElement> = inject(ElementRef);
  private renderer = inject(Renderer2);
  private ngZone = inject(NgZone);

  @Input() set dndDisableIf(value: boolean) {
    this.draggable = !value;

    if (this.draggable) {
      this.renderer.removeClass(
        this.elementRef.nativeElement,
        this.dndDraggableDisabledClass
      );
    } else {
      this.renderer.addClass(
        this.elementRef.nativeElement,
        this.dndDraggableDisabledClass
      );
    }
  }

  @Input() set dndDisableDragIf(value: boolean) {
    this.dndDisableIf = value;
  }

  ngAfterViewInit(): void {
    this.ngZone.runOutsideAngular(() => {
      this.elementRef.nativeElement.addEventListener(
        'drag',
        this.dragEventHandler
      );
    });
  }

  ngOnDestroy(): void {
    this.elementRef.nativeElement.removeEventListener(
      'drag',
      this.dragEventHandler
    );
    if (this.isDragStarted) {
      endDrag();
    }
  }

  @HostListener('dragstart', ['$event']) onDragStart(event: DndEvent): boolean {
    if (!this.draggable) {
      return false;
    }

    // check if there is dnd handle and if the dnd handle was used to start the drag
    if (this.dndHandle != null && event._dndUsingHandle == null) {
      event.stopPropagation();
      return false;
    }

    // initialize global state
    startDrag(event, this.dndEffectAllowed, this.dndType);

    this.isDragStarted = true;

    setDragData(
      event,
      { data: this.dndDraggable, type: this.dndType },
      dndState.effectAllowed!
    );

    this.dragImage = this.determineDragImage();

    // set dragging css class prior to setDragImage so styles are applied before
    // TODO breaking change: add class to elementRef rather than drag image which could be another element
    this.renderer.addClass(this.dragImage, this.dndDraggingClass);

    // set custom dragimage if present
    // set dragimage if drag is started from dndHandle
    if (this.dndDragImageElementRef != null || event._dndUsingHandle != null) {
      setDragImage(event, this.dragImage, this.dndDragImageOffsetFunction);
    }

    // add dragging source css class on first drag event
    const unregister = this.renderer.listen(
      this.elementRef.nativeElement,
      'drag',
      () => {
        this.renderer.addClass(
          this.elementRef.nativeElement,
          this.dndDraggingSourceClass
        );
        unregister();
      }
    );

    this.dndStart.emit(event);

    event.stopPropagation();

    setTimeout(() => {
      if (this.isDragStarted) {
        this.renderer.setStyle(this.dragImage, 'pointer-events', 'none');
      }
    }, 100);

    return true;
  }

  onDrag(event: DragEvent) {
    this.dndDrag.emit(event);
  }

  @HostListener('dragend', ['$event']) onDragEnd(event: DragEvent) {
    if (!this.draggable || !this.isDragStarted) {
      return;
    }
    // get drop effect from custom stored state as its not reliable across browsers
    const dropEffect = dndState.dropEffect;

    this.renderer.setStyle(this.dragImage, 'pointer-events', 'unset');

    let dropEffectEmitter: EventEmitter<DragEvent>;

    switch (dropEffect) {
      case 'copy':
        dropEffectEmitter = this.dndCopied;
        break;

      case 'link':
        dropEffectEmitter = this.dndLinked;
        break;

      case 'move':
        dropEffectEmitter = this.dndMoved;
        break;

      default:
        dropEffectEmitter = this.dndCanceled;
        break;
    }

    dropEffectEmitter.emit(event);
    this.dndEnd.emit(event);

    // reset global state
    endDrag();

    this.isDragStarted = false;

    this.renderer.removeClass(this.dragImage, this.dndDraggingClass);

    // IE9 special hammering
    window.setTimeout(() => {
      this.renderer.removeClass(
        this.elementRef.nativeElement,
        this.dndDraggingSourceClass
      );
    }, 0);

    event.stopPropagation();
  }

  registerDragHandle(handle: DndHandleDirective | undefined) {
    this.dndHandle = handle;
  }

  registerDragImage(elementRef: ElementRef | undefined) {
    this.dndDragImageElementRef = elementRef;
  }

  private readonly dragEventHandler: (event: DragEvent) => void = (
    event: DragEvent
  ) => this.onDrag(event);

  private determineDragImage(): Element {
    // evaluate custom drag image existence
    if (typeof this.dndDragImageElementRef !== 'undefined') {
      return this.dndDragImageElementRef.nativeElement as Element;
    } else {
      return this.elementRef.nativeElement;
    }
  }
}


================================================
FILE: projects/dnd/src/lib/dnd-dropzone.directive.spec.ts
================================================
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { describe, it, expect, beforeEach } from 'vitest';
import {
  DndDropzoneDirective,
  DndPlaceholderRefDirective,
} from './dnd-dropzone.directive';
import { endDrag } from './dnd-state';

@Component({
  standalone: true,
  imports: [DndDropzoneDirective],
  template: `<div dndDropzone>drop here</div>`,
})
class BasicDropzoneHost {}

@Component({
  standalone: true,
  imports: [DndDropzoneDirective],
  template: `<div dndDropzone>drop here</div>`,
})
class DisabledDropzoneHost {}

@Component({
  standalone: true,
  imports: [DndDropzoneDirective, DndPlaceholderRefDirective],
  template: `
    <div dndDropzone>
      <div dndPlaceholderRef>placeholder</div>
      <div class="item">item 1</div>
    </div>
  `,
})
class PlaceholderDropzoneHost {}

@Component({
  standalone: true,
  imports: [DndDropzoneDirective],
  template: `<div [dndDropzone]="['typeA', 'typeB']">typed drop</div>`,
})
class TypedDropzoneHost {}

describe('DndDropzoneDirective', () => {
  let fixture: ComponentFixture<BasicDropzoneHost>;
  let dropzoneEl: DebugElement;

  beforeEach(async () => {
    endDrag();
    await TestBed.configureTestingModule({
      imports: [BasicDropzoneHost],
    }).compileComponents();

    fixture = TestBed.createComponent(BasicDropzoneHost);
    fixture.detectChanges();
    dropzoneEl = fixture.debugElement.query(By.directive(DndDropzoneDirective));
  });

  it('should have the directive instance', () => {
    const directive = dropzoneEl.injector.get(DndDropzoneDirective);
    expect(directive).toBeTruthy();
  });

  it('should have default dndEffectAllowed', () => {
    const directive = dropzoneEl.injector.get(DndDropzoneDirective);
    expect(directive.dndEffectAllowed).toBe('uninitialized');
  });

  it('should have default dndDragoverClass', () => {
    const directive = dropzoneEl.injector.get(DndDropzoneDirective);
    expect(directive.dndDragoverClass).toBe('dndDragover');
  });
});

describe('DndDropzoneDirective - disabled', () => {
  let fixture: ComponentFixture<DisabledDropzoneHost>;
  let directive: DndDropzoneDirective;
  let dropzoneEl: DebugElement;

  beforeEach(async () => {
    endDrag();
    await TestBed.configureTestingModule({
      imports: [DisabledDropzoneHost],
    }).compileComponents();

    fixture = TestBed.createComponent(DisabledDropzoneHost);
    fixture.detectChanges();
    dropzoneEl = fixture.debugElement.query(By.directive(DndDropzoneDirective));
    directive = dropzoneEl.injector.get(DndDropzoneDirective);
  });

  it('should add disabled class when disabled', () => {
    directive.dndDisableIf = true;
    fixture.detectChanges();
    expect(
      dropzoneEl.nativeElement.classList.contains('dndDropzoneDisabled')
    ).toBe(true);
  });

  it('should remove disabled class when re-enabled', () => {
    directive.dndDisableIf = true;
    fixture.detectChanges();
    directive.dndDisableIf = false;
    fixture.detectChanges();
    expect(
      dropzoneEl.nativeElement.classList.contains('dndDropzoneDisabled')
    ).toBe(false);
  });
});

describe('DndDropzoneDirective - placeholder', () => {
  let fixture: ComponentFixture<PlaceholderDropzoneHost>;
  let dropzoneEl: DebugElement;

  beforeEach(async () => {
    endDrag();
    await TestBed.configureTestingModule({
      imports: [PlaceholderDropzoneHost],
    }).compileComponents();

    fixture = TestBed.createComponent(PlaceholderDropzoneHost);
    fixture.detectChanges();
    dropzoneEl = fixture.debugElement.query(By.directive(DndDropzoneDirective));
  });

  it('should remove placeholder from DOM on init', () => {
    const placeholder = dropzoneEl.nativeElement.querySelector(
      '[dndPlaceholderRef]'
    );
    expect(placeholder).toBeNull();
  });
});

describe('DndDropzoneDirective - typed', () => {
  let fixture: ComponentFixture<TypedDropzoneHost>;
  let dropzoneEl: DebugElement;

  beforeEach(async () => {
    endDrag();
    await TestBed.configureTestingModule({
      imports: [TypedDropzoneHost],
    }).compileComponents();

    fixture = TestBed.createComponent(TypedDropzoneHost);
    fixture.detectChanges();
    dropzoneEl = fixture.debugElement.query(By.directive(DndDropzoneDirective));
  });

  it('should accept typed dropzone input', () => {
    const directive = dropzoneEl.injector.get(DndDropzoneDirective);
    expect(directive.dndDropzone).toEqual(['typeA', 'typeB']);
  });
});


================================================
FILE: projects/dnd/src/lib/dnd-dropzone.directive.ts
================================================
import {
  AfterViewInit,
  ContentChild,
  Directive,
  ElementRef,
  EventEmitter,
  HostListener,
  Input,
  NgZone,
  OnDestroy,
  OnInit,
  Output,
  Renderer2,
} from '@angular/core';
import {
  getDndType,
  getDropEffect,
  isExternalDrag,
  setDropEffect,
} from './dnd-state';
import { DropEffect, EffectAllowed } from './dnd-types';
import {
  DndEvent,
  DragDropData,
  getDirectChildElement,
  getDropData,
  shouldPositionPlaceholderBeforeElement,
} from './dnd-utils';

export interface DndDropEvent {
  event: DragEvent;
  dropEffect: DropEffect;
  isExternal: boolean;
  data?: any;
  index?: number;
  type?: any;
}

@Directive({ selector: '[dndPlaceholderRef]', standalone: true })
export class DndPlaceholderRefDirective implements OnInit {
  constructor(public readonly elementRef: ElementRef<HTMLElement>) {}

  ngOnInit() {
    // placeholder has to be "invisible" to the cursor, or it would interfere with the dragover detection for the same dropzone
    this.elementRef.nativeElement.style.pointerEvents = 'none';
  }
}

@Directive({ selector: '[dndDropzone]', standalone: true })
export class DndDropzoneDirective implements AfterViewInit, OnDestroy {
  @Input() dndDropzone?: string[] | '' = '';

  @Input() dndEffectAllowed: EffectAllowed = 'uninitialized';

  @Input() dndAllowExternal: boolean = false;

  @Input() dndHorizontal: boolean = false;

  @Input() dndDragoverClass: string = 'dndDragover';

  @Input() dndDropzoneDisabledClass = 'dndDropzoneDisabled';

  @Output() readonly dndDragover: EventEmitter<DragEvent> =
    new EventEmitter<DragEvent>();

  @Output() readonly dndDrop: EventEmitter<DndDropEvent> =
    new EventEmitter<DndDropEvent>();

  @ContentChild(DndPlaceholderRefDirective)
  private readonly dndPlaceholderRef?: DndPlaceholderRefDirective;

  private placeholder: Element | null = null;

  private disabled: boolean = false;

  private enterCount: number = 0;

  constructor(
    private ngZone: NgZone,
    private elementRef: ElementRef,
    private renderer: Renderer2
  ) {}

  @Input() set dndDisableIf(value: boolean) {
    this.disabled = value;

    if (this.disabled) {
      this.renderer.addClass(
        this.elementRef.nativeElement,
        this.dndDropzoneDisabledClass
      );
    } else {
      this.renderer.removeClass(
        this.elementRef.nativeElement,
        this.dndDropzoneDisabledClass
      );
    }
  }

  @Input() set dndDisableDropIf(value: boolean) {
    this.dndDisableIf = value;
  }

  ngAfterViewInit(): void {
    this.placeholder = this.tryGetPlaceholder();

    this.removePlaceholderFromDOM();

    this.ngZone.runOutsideAngular(() => {
      this.elementRef.nativeElement.addEventListener(
        'dragenter',
        this.dragEnterEventHandler
      );
      this.elementRef.nativeElement.addEventListener(
        'dragover',
        this.dragOverEventHandler
      );
      this.elementRef.nativeElement.addEventListener(
        'dragleave',
        this.dragLeaveEventHandler
      );
    });
  }

  ngOnDestroy(): void {
    this.elementRef.nativeElement.removeEventListener(
      'dragenter',
      this.dragEnterEventHandler
    );
    this.elementRef.nativeElement.removeEventListener(
      'dragover',
      this.dragOverEventHandler
    );
    this.elementRef.nativeElement.removeEventListener(
      'dragleave',
      this.dragLeaveEventHandler
    );
  }

  onDragEnter(event: DndEvent) {
    this.enterCount++;

    // check if another dropzone is activated
    if (event._dndDropzoneActive === true) {
      this.removePlaceholderFromDOM();
      this.renderer.removeClass(
        this.elementRef.nativeElement,
        this.dndDragoverClass
      );
      return;
    }

    // set as active if the target element is inside this dropzone
    if (event._dndDropzoneActive == null) {
      if (this.elementRef.nativeElement.contains(event.target)) {
        event._dndDropzoneActive = true;
      }
    }

    // check if this drag event is allowed to drop on this dropzone
    const type = getDndType(event);
    if (!this.isDropAllowed(type)) {
      return;
    }

    // allow the dragenter
    event.preventDefault();
  }

  onDragOver(event: DragEvent) {
    // With nested dropzones, we want to ignore this event if a child dropzone
    // has already handled a dragover.  Historically, event.stopPropagation() was
    // used to prevent this bubbling, but that prevents any dragovers outside the
    // ngx-drag-drop component, and stops other use cases such as scrolling on drag.
    // Instead, we can check if the event was already prevented by a child and bail early.
    if (event.defaultPrevented) {
      return;
    }

    // check if this drag event is allowed to drop on this dropzone
    const type = getDndType(event);
    if (!this.isDropAllowed(type)) {
      return;
    }

    this.checkAndUpdatePlaceholderPosition(event);

    const dropEffect = getDropEffect(event, this.dndEffectAllowed);

    if (dropEffect === 'none') {
      this.cleanupDragoverState();
      return;
    }

    // allow the dragover
    event.preventDefault();

    // set the drop effect
    setDropEffect(event, dropEffect);

    this.dndDragover.emit(event);

    this.renderer.addClass(
      this.elementRef.nativeElement,
      this.dndDragoverClass
    );
  }

  @HostListener('drop', ['$event']) onDrop(event: DragEvent) {
    try {
      // check if this drag event is allowed to drop on this dropzone
      const type = getDndType(event);
      if (!this.isDropAllowed(type)) {
        return;
      }

      const data: DragDropData = getDropData(event, isExternalDrag());

      if (!this.isDropAllowed(data.type)) {
        return;
      }

      // signal custom drop handling
      event.preventDefault();

      const dropEffect = getDropEffect(event, this.dndEffectAllowed);

      setDropEffect(event, dropEffect);

      if (dropEffect === 'none') {
        return;
      }

      const dropIndex = this.getPlaceholderIndex();

      // if for whatever reason the placeholder is not present in the DOM but it should be there
      // we don't allow/emit the drop event since it breaks the contract
      // seems to only happen if drag and drop is executed faster than the DOM updates
      if (dropIndex === -1) {
        return;
      }

      this.dndDrop.emit({
        event: event,
        dropEffect: dropEffect,
        isExternal: isExternalDrag(),
        data: data.data,
        index: dropIndex,
        type: type,
      });

      event.stopPropagation();
    } finally {
      this.cleanupDragoverState();
    }
  }

  onDragLeave(event: DndEvent) {
    this.enterCount--;

    // only clean up when all enter/leave pairs are balanced (cursor has truly left)
    if (this.enterCount > 0) {
      return;
    }

    this.cleanupDragoverState();

    // cleanup drop effect when leaving dropzone
    setDropEffect(event, 'none');
  }

  private readonly dragEnterEventHandler: (event: DragEvent) => void = (
    event: DragEvent
  ) => this.onDragEnter(event);

  private readonly dragOverEventHandler: (event: DragEvent) => void = (
    event: DragEvent
  ) => this.onDragOver(event);

  private readonly dragLeaveEventHandler: (event: DragEvent) => void = (
    event: DragEvent
  ) => this.onDragLeave(event);

  private isDropAllowed(type?: string): boolean {
    // dropzone is disabled -> deny it
    if (this.disabled) {
      return false;
    }

    // if drag did not start from our directive
    // and external drag sources are not allowed -> deny it
    if (isExternalDrag() && !this.dndAllowExternal) {
      return false;
    }

    // no filtering by types -> allow it
    if (!this.dndDropzone) {
      return true;
    }

    // no type set -> allow it
    if (!type) {
      return true;
    }

    if (!Array.isArray(this.dndDropzone)) {
      throw new Error(
        'dndDropzone: bound value to [dndDropzone] must be an array!'
      );
    }

    // if dropzone contains type -> allow it
    return this.dndDropzone.indexOf(type) !== -1;
  }

  private tryGetPlaceholder(): Element | null {
    if (typeof this.dndPlaceholderRef !== 'undefined') {
      return this.dndPlaceholderRef.elementRef.nativeElement as Element;
    }

    // TODO nasty workaround needed because if ng-container / template is used @ContentChild() or DI will fail because
    // of wrong context see angular bug https://github.com/angular/angular/issues/13517
    return this.elementRef.nativeElement.querySelector('[dndPlaceholderRef]');
  }

  private removePlaceholderFromDOM() {
    if (this.placeholder !== null && this.placeholder.parentNode !== null) {
      this.placeholder.parentNode.removeChild(this.placeholder);
    }
  }

  private checkAndUpdatePlaceholderPosition(event: DragEvent): void {
    if (this.placeholder === null) {
      return;
    }

    // make sure the placeholder is in the DOM
    if (this.placeholder.parentNode !== this.elementRef.nativeElement) {
      this.renderer.appendChild(
        this.elementRef.nativeElement,
        this.placeholder
      );
    }

    // update the position if the event originates from a child element of the dropzone
    const directChild = getDirectChildElement(
      this.elementRef.nativeElement,
      event.target as Element
    );

    // early exit if no direct child or direct child is placeholder
    if (directChild === null || directChild === this.placeholder) {
      return;
    }

    const positionPlaceholderBeforeDirectChild =
      shouldPositionPlaceholderBeforeElement(
        event,
        directChild,
        this.dndHorizontal
      );

    if (positionPlaceholderBeforeDirectChild) {
      // do insert before only if necessary
      if (directChild.previousSibling !== this.placeholder) {
        this.renderer.insertBefore(
          this.elementRef.nativeElement,
          this.placeholder,
          directChild
        );
      }
    } else {
      // do insert after only if necessary
      if (directChild.nextSibling !== this.placeholder) {
        this.renderer.insertBefore(
          this.elementRef.nativeElement,
          this.placeholder,
          directChild.nextSibling
        );
      }
    }
  }

  private getPlaceholderIndex(): number | undefined {
    if (this.placeholder === null) {
      return undefined;
    }

    const element = this.elementRef.nativeElement as HTMLElement;

    return Array.prototype.indexOf.call(element.children, this.placeholder);
  }

  private cleanupDragoverState() {
    this.renderer.removeClass(
      this.elementRef.nativeElement,
      this.dndDragoverClass
    );

    this.enterCount = 0;
    this.removePlaceholderFromDOM();
  }
}


================================================
FILE: projects/dnd/src/lib/dnd-handle.directive.spec.ts
================================================
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { describe, it, expect, beforeEach } from 'vitest';
import { DndDraggableDirective } from './dnd-draggable.directive';
import { DndHandleDirective } from './dnd-handle.directive';
import { endDrag } from './dnd-state';

@Component({
  standalone: true,
  imports: [DndDraggableDirective, DndHandleDirective],
  template: `
    <div [dndDraggable]="'data'">
      <div dndHandle class="handle">handle</div>
      content
    </div>
  `,
})
class HandleHost {}

describe('DndHandleDirective', () => {
  let fixture: ComponentFixture<HandleHost>;
  let handleEl: DebugElement;

  beforeEach(async () => {
    endDrag();
    await TestBed.configureTestingModule({
      imports: [HandleHost],
    }).compileComponents();

    fixture = TestBed.createComponent(HandleHost);
    fixture.detectChanges();
    handleEl = fixture.debugElement.query(By.directive(DndHandleDirective));
  });

  it('should set draggable attribute on the handle', () => {
    expect(handleEl.nativeElement.getAttribute('draggable')).toBe('true');
  });

  it('should set _dndUsingHandle on dragstart event', () => {
    const directive = handleEl.injector.get(DndHandleDirective);
    const event = {} as any;
    directive.onDragEvent(event);
    expect(event._dndUsingHandle).toBe(true);
  });

  it('should set _dndUsingHandle on dragend event', () => {
    const directive = handleEl.injector.get(DndHandleDirective);
    const event = {} as any;
    directive.onDragEvent(event);
    expect(event._dndUsingHandle).toBe(true);
  });

  it('should unregister handle on destroy', () => {
    const draggableEl = fixture.debugElement.query(
      By.directive(DndDraggableDirective)
    );
    const directive = draggableEl.injector.get(DndDraggableDirective);
    expect((directive as any).dndHandle).toBeTruthy();

    fixture.destroy();
    expect((directive as any).dndHandle).toBeUndefined();
  });
});


================================================
FILE: projects/dnd/src/lib/dnd-handle.directive.ts
================================================
import {
  Directive,
  HostBinding,
  HostListener,
  inject,
  OnDestroy,
  OnInit,
} from '@angular/core';
import { DndDraggableDirective } from './dnd-draggable.directive';
import { DndEvent } from './dnd-utils';

@Directive({ selector: '[dndHandle]', standalone: true })
export class DndHandleDirective implements OnInit, OnDestroy {
  @HostBinding('attr.draggable') draggable = true;

  dndDraggableDirective = inject(DndDraggableDirective);

  ngOnInit() {
    this.dndDraggableDirective.registerDragHandle(this);
  }

  ngOnDestroy(): void {
    this.dndDraggableDirective.registerDragHandle(undefined);
  }

  @HostListener('dragstart', ['$event'])
  @HostListener('dragend', ['$event'])
  onDragEvent(event: DndEvent) {
    event._dndUsingHandle = true;
  }
}


================================================
FILE: projects/dnd/src/lib/dnd-state.spec.ts
================================================
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
  startDrag,
  endDrag,
  setDropEffect,
  getDropEffect,
  getDndType,
  isExternalDrag,
  dndState,
} from './dnd-state';
import { CUSTOM_MIME_TYPE } from './dnd-utils';

function createMockDragEvent(overrides: Partial<DragEvent> = {}): DragEvent {
  return {
    dataTransfer: {
      effectAllowed: 'all',
      dropEffect: 'none',
      types: [],
    },
    ctrlKey: false,
    altKey: false,
    ...overrides,
  } as unknown as DragEvent;
}

describe('dnd-state', () => {
  beforeEach(() => {
    endDrag();
  });

  describe('startDrag', () => {
    it('should set isDragging to true', () => {
      const event = createMockDragEvent();
      startDrag(event, 'all', undefined);
      expect(dndState.isDragging).toBe(true);
    });

    it('should set effectAllowed on state and dataTransfer', () => {
      const event = createMockDragEvent();
      startDrag(event, 'copyMove', undefined);
      expect(dndState.effectAllowed).toBe('copyMove');
      expect(event.dataTransfer!.effectAllowed).toBe('copyMove');
    });

    it('should set type', () => {
      const event = createMockDragEvent();
      startDrag(event, 'all', 'myType');
      expect(dndState.type).toBe('myType');
    });

    it('should reset dropEffect to none', () => {
      const event = createMockDragEvent();
      startDrag(event, 'all', undefined);
      expect(dndState.dropEffect).toBe('none');
    });
  });

  describe('endDrag', () => {
    it('should reset all state', () => {
      const event = createMockDragEvent();
      startDrag(event, 'copyMove', 'test');
      endDrag();

      expect(dndState.isDragging).toBe(false);
      expect(dndState.dropEffect).toBeUndefined();
      expect(dndState.effectAllowed).toBeUndefined();
      expect(dndState.type).toBeUndefined();
    });
  });

  describe('setDropEffect', () => {
    it('should update state when dragging', () => {
      const event = createMockDragEvent();
      startDrag(event, 'all', undefined);

      setDropEffect(event, 'copy');
      expect(dndState.dropEffect).toBe('copy');
      expect(event.dataTransfer!.dropEffect).toBe('copy');
    });

    it('should not update state when not dragging', () => {
      const event = createMockDragEvent();
      setDropEffect(event, 'copy');
      expect(dndState.dropEffect).toBeUndefined();
      expect(event.dataTransfer!.dropEffect).toBe('copy');
    });
  });

  describe('getDropEffect', () => {
    it('should return first available effect', () => {
      const event = createMockDragEvent();
      expect(getDropEffect(event)).toBe('move');
    });

    it('should return "none" when no effects available', () => {
      const event = createMockDragEvent({
        dataTransfer: {
          effectAllowed: 'none',
        } as DataTransfer,
      });
      expect(getDropEffect(event)).toBe('none');
    });

    it('should filter by dndState effectAllowed when dragging', () => {
      const startEvent = createMockDragEvent();
      startDrag(startEvent, 'copy', undefined);

      const event = createMockDragEvent();
      expect(getDropEffect(event)).toBe('copy');
    });

    it('should filter by provided effectAllowed', () => {
      const event = createMockDragEvent();
      expect(getDropEffect(event, 'link')).toBe('link');
    });

    it('should return "copy" when ctrlKey is held and copy is available', () => {
      const event = createMockDragEvent({ ctrlKey: true } as any);
      expect(getDropEffect(event)).toBe('copy');
    });

    it('should return "link" when altKey is held and link is available', () => {
      const event = createMockDragEvent({ altKey: true } as any);
      expect(getDropEffect(event)).toBe('link');
    });

    it('should handle missing dataTransfer', () => {
      const event = {
        dataTransfer: null,
        ctrlKey: false,
        altKey: false,
      } as unknown as DragEvent;
      expect(getDropEffect(event)).toBe('move');
    });
  });

  describe('getDndType', () => {
    it('should return type from state when dragging', () => {
      const event = createMockDragEvent();
      startDrag(event, 'all', 'myType');
      expect(getDndType(event)).toBe('myType');
    });

    it('should return undefined from state when dragging with no type', () => {
      const event = createMockDragEvent();
      startDrag(event, 'all', undefined);
      expect(getDndType(event)).toBeUndefined();
    });

    it('should extract type from custom MIME type for external drag', () => {
      const event = {
        dataTransfer: {
          types: [CUSTOM_MIME_TYPE + '-myType'],
        },
      } as unknown as DragEvent;
      expect(getDndType(event)).toBe('myType');
    });

    it('should return undefined when no known MIME type on external drag', () => {
      const event = {
        dataTransfer: {
          types: ['text/plain'],
        },
      } as unknown as DragEvent;
      expect(getDndType(event)).toBeUndefined();
    });
  });

  describe('isExternalDrag', () => {
    it('should return true when not dragging', () => {
      expect(isExternalDrag()).toBe(true);
    });

    it('should return false when dragging', () => {
      const event = createMockDragEvent();
      startDrag(event, 'all', undefined);
      expect(isExternalDrag()).toBe(false);
    });
  });
});


================================================
FILE: projects/dnd/src/lib/dnd-state.ts
================================================
import { DropEffect, EffectAllowed } from './dnd-types';
import {
  CUSTOM_MIME_TYPE,
  DROP_EFFECTS,
  filterEffects,
  getWellKnownMimeType,
} from './dnd-utils';

export interface DndState {
  isDragging: boolean;
  dropEffect?: DropEffect;
  effectAllowed?: EffectAllowed;
  type?: string;
}

const _dndState: DndState = {
  isDragging: false,
  dropEffect: 'none',
  effectAllowed: 'all',
  type: undefined,
};

export function startDrag(
  event: DragEvent,
  effectAllowed: EffectAllowed,
  type: string | undefined
) {
  _dndState.isDragging = true;
  _dndState.dropEffect = 'none';
  _dndState.effectAllowed = effectAllowed;
  _dndState.type = type;

  if (event.dataTransfer) {
    event.dataTransfer.effectAllowed = effectAllowed;
  }
}

export function endDrag() {
  _dndState.isDragging = false;
  _dndState.dropEffect = undefined;
  _dndState.effectAllowed = undefined;
  _dndState.type = undefined;
}

export function setDropEffect(event: DragEvent, dropEffect: DropEffect) {
  if (_dndState.isDragging === true) {
    _dndState.dropEffect = dropEffect;
  }

  if (event.dataTransfer) {
    event.dataTransfer.dropEffect = dropEffect;
  }
}

export function getDropEffect(
  event: DragEvent,
  effectAllowed?: EffectAllowed | DropEffect
): DropEffect {
  const dataTransferEffectAllowed: EffectAllowed = event.dataTransfer
    ? (event.dataTransfer.effectAllowed as EffectAllowed)
    : 'uninitialized';

  let effects = filterEffects(DROP_EFFECTS, dataTransferEffectAllowed);

  if (_dndState.isDragging === true) {
    effects = filterEffects(effects, _dndState.effectAllowed!);
  }

  if (effectAllowed) {
    effects = filterEffects(effects, effectAllowed);
  }

  // MacOS automatically filters dataTransfer.effectAllowed depending on the modifier keys,
  // therefore the following modifier keys will only affect other operating systems.
  if (effects.length === 0) {
    return 'none';
  }

  if (event.ctrlKey && effects.indexOf('copy') !== -1) {
    return 'copy';
  }

  if (event.altKey && effects.indexOf('link') !== -1) {
    return 'link';
  }

  return effects[0] as DropEffect;
}

export function getDndType(event: DragEvent): string | undefined {
  if (_dndState.isDragging === true) {
    return _dndState.type;
  }

  const mimeType = getWellKnownMimeType(event);

  if (mimeType === null) {
    return undefined;
  }

  return mimeType.substring(CUSTOM_MIME_TYPE.length + 1) || undefined;
}

export function isExternalDrag(): boolean {
  return _dndState.isDragging === false;
}

export const dndState: Readonly<DndState> = _dndState as Readonly<DndState>;


================================================
FILE: projects/dnd/src/lib/dnd-types.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'
  | 'uninitialized';


================================================
FILE: projects/dnd/src/lib/dnd-utils.spec.ts
================================================
import { describe, it, expect, vi } from 'vitest';
import {
  filterEffects,
  getWellKnownMimeType,
  setDragData,
  getDropData,
  getDirectChildElement,
  shouldPositionPlaceholderBeforeElement,
  calculateDragImageOffset,
  setDragImage,
  CUSTOM_MIME_TYPE,
} from './dnd-utils';

function createMockDragEvent(overrides: Partial<DragEvent> = {}): DragEvent {
  const dataStore = new Map<string, string>();
  return {
    dataTransfer: {
      types: [] as string[],
      effectAllowed: 'all',
      setData: vi.fn((type: string, data: string) => {
        dataStore.set(type, data);
        (overrides.dataTransfer as any)?.types?.push?.(type);
      }),
      getData: vi.fn((type: string) => dataStore.get(type) ?? ''),
      setDragImage: vi.fn(),
    },
    ...overrides,
  } as unknown as DragEvent;
}

describe('filterEffects', () => {
  const allEffects = ['move', 'copy', 'link'] as any;

  it('should return all effects for "all"', () => {
    expect(filterEffects(allEffects, 'all')).toEqual(['move', 'copy', 'link']);
  });

  it('should return all effects for "uninitialized"', () => {
    expect(filterEffects(allEffects, 'uninitialized')).toEqual([
      'move',
      'copy',
      'link',
    ]);
  });

  it('should filter to "copyMove"', () => {
    expect(filterEffects(allEffects, 'copyMove')).toEqual(['move', 'copy']);
  });

  it('should filter to "copyLink"', () => {
    expect(filterEffects(allEffects, 'copyLink')).toEqual(['copy', 'link']);
  });

  it('should filter to "linkMove"', () => {
    expect(filterEffects(allEffects, 'linkMove')).toEqual(['move', 'link']);
  });

  it('should return single effect for exact match', () => {
    expect(filterEffects(allEffects, 'copy')).toEqual(['copy']);
  });

  it('should return empty for "none"', () => {
    expect(filterEffects(allEffects, 'none')).toEqual([]);
  });
});

describe('getWellKnownMimeType', () => {
  it('should return null when no dataTransfer', () => {
    const event = { dataTransfer: null } as DragEvent;
    expect(getWellKnownMimeType(event)).toBeNull();
  });

  it('should return custom MIME type when present', () => {
    const customType = CUSTOM_MIME_TYPE + '-mytype';
    const event = {
      dataTransfer: { types: [customType] },
    } as unknown as DragEvent;
    expect(getWellKnownMimeType(event)).toBe(customType);
  });

  it('should return null for unknown MIME types', () => {
    const event = {
      dataTransfer: { types: ['text/plain'] },
    } as unknown as DragEvent;
    expect(getWellKnownMimeType(event)).toBeNull();
  });

  it('should return null for non-custom known MIME types', () => {
    const event = {
      dataTransfer: { types: ['application/json'] },
    } as unknown as DragEvent;
    expect(getWellKnownMimeType(event)).toBeNull();
  });
});

describe('setDragData', () => {
  it('should set data with custom MIME type', () => {
    const event = createMockDragEvent();
    const data = { data: 'test', type: 'mytype' };
    setDragData(event, data, 'all');
    expect(event.dataTransfer!.setData).toHaveBeenCalledWith(
      CUSTOM_MIME_TYPE + '-mytype',
      JSON.stringify(data)
    );
  });

  it('should set data without type suffix when no type', () => {
    const event = createMockDragEvent();
    const data = { data: 'test' };
    setDragData(event, data, 'all');
    expect(event.dataTransfer!.setData).toHaveBeenCalledWith(
      CUSTOM_MIME_TYPE,
      JSON.stringify(data)
    );
  });
});

describe('getDropData', () => {
  it('should parse data from custom MIME type for internal drag', () => {
    const payload = { data: 'hello', type: 'mytype' };
    const mimeType = CUSTOM_MIME_TYPE + '-mytype';
    const event = {
      dataTransfer: {
        types: [mimeType],
        getData: vi.fn(() => JSON.stringify(payload)),
      },
    } as unknown as DragEvent;

    expect(getDropData(event, false)).toEqual(payload);
  });

  it('should return empty object for unknown MIME type on internal drag', () => {
    const event = {
      dataTransfer: {
        types: ['text/plain'],
        getData: vi.fn(() => ''),
      },
    } as unknown as DragEvent;

    expect(getDropData(event, false)).toEqual({});
  });

  it('should return empty object for external drag with unknown MIME type', () => {
    const event = {
      dataTransfer: {
        types: ['text/plain'],
        getData: vi.fn(() => ''),
      },
    } as unknown as DragEvent;

    expect(getDropData(event, true)).toEqual({});
  });

  it('should parse data for external drag with custom MIME type', () => {
    const payload = { data: 'ext' };
    const mimeType = CUSTOM_MIME_TYPE;
    const event = {
      dataTransfer: {
        types: [mimeType],
        getData: vi.fn(() => JSON.stringify(payload)),
      },
    } as unknown as DragEvent;

    expect(getDropData(event, true)).toEqual(payload);
  });
});

describe('getDirectChildElement', () => {
  it('should return the direct child', () => {
    const parent = document.createElement('div');
    const child = document.createElement('span');
    const grandchild = document.createElement('a');
    parent.appendChild(child);
    child.appendChild(grandchild);

    expect(getDirectChildElement(parent, grandchild)).toBe(child);
  });

  it('should return the element itself when it is a direct child', () => {
    const parent = document.createElement('div');
    const child = document.createElement('span');
    parent.appendChild(child);

    expect(getDirectChildElement(parent, child)).toBe(child);
  });

  it('should return null when parent is not an ancestor', () => {
    const parent = document.createElement('div');
    const unrelated = document.createElement('span');

    expect(getDirectChildElement(parent, unrelated)).toBeNull();
  });
});

describe('shouldPositionPlaceholderBeforeElement', () => {
  it('should return true when cursor is in upper half (vertical)', () => {
    const element = document.createElement('div');
    vi.spyOn(element, 'getBoundingClientRect').mockReturnValue({
      top: 100,
      height: 50,
      left: 0,
      width: 100,
    } as DOMRect);

    const event = { clientY: 110, clientX: 0 } as DragEvent;
    expect(shouldPositionPlaceholderBeforeElement(event, element, false)).toBe(
      true
    );
  });

  it('should return false when cursor is in lower half (vertical)', () => {
    const element = document.createElement('div');
    vi.spyOn(element, 'getBoundingClientRect').mockReturnValue({
      top: 100,
      height: 50,
      left: 0,
      width: 100,
    } as DOMRect);

    const event = { clientY: 140, clientX: 0 } as DragEvent;
    expect(shouldPositionPlaceholderBeforeElement(event, element, false)).toBe(
      false
    );
  });

  it('should return true when cursor is in left half (horizontal)', () => {
    const element = document.createElement('div');
    vi.spyOn(element, 'getBoundingClientRect').mockReturnValue({
      top: 0,
      height: 50,
      left: 100,
      width: 100,
    } as DOMRect);

    const event = { clientX: 120, clientY: 0 } as DragEvent;
    expect(shouldPositionPlaceholderBeforeElement(event, element, true)).toBe(
      true
    );
  });

  it('should return false when cursor is in right half (horizontal)', () => {
    const element = document.createElement('div');
    vi.spyOn(element, 'getBoundingClientRect').mockReturnValue({
      top: 0,
      height: 50,
      left: 100,
      width: 100,
    } as DOMRect);

    const event = { clientX: 180, clientY: 0 } as DragEvent;
    expect(shouldPositionPlaceholderBeforeElement(event, element, true)).toBe(
      false
    );
  });
});

describe('calculateDragImageOffset', () => {
  it('should calculate offset with padding and border', () => {
    const element = document.createElement('div');
    vi.spyOn(window, 'getComputedStyle').mockReturnValue({
      paddingTop: '10px',
      paddingLeft: '5px',
      borderTopWidth: '2px',
      borderLeftWidth: '3px',
    } as CSSStyleDeclaration);

    const event = { offsetX: 20, offsetY: 30 } as DragEvent;
    const result = calculateDragImageOffset(event, element);

    expect(result).toEqual({ x: 28, y: 42 });
  });
});

describe('setDragImage', () => {
  it('should call setDragImage on dataTransfer with offset', () => {
    const setDragImageFn = vi.fn();
    const event = {
      dataTransfer: { setDragImage: setDragImageFn },
    } as unknown as DragEvent;
    const element = document.createElement('div');
    const offsetFn = vi.fn().mockReturnValue({ x: 10, y: 20 });

    setDragImage(event, element, offsetFn);

    expect(offsetFn).toHaveBeenCalledWith(event, element);
    expect(setDragImageFn).toHaveBeenCalledWith(element, 10, 20);
  });
});


================================================
FILE: projects/dnd/src/lib/dnd-utils.ts
================================================
import { DropEffect, EffectAllowed } from './dnd-types';

export interface DragDropData {
  data?: any;
  type?: string;
}

export interface DndEvent extends DragEvent {
  _dndUsingHandle?: boolean;
  _dndDropzoneActive?: true;
}

export type DndDragImageOffsetFunction = (
  event: DragEvent,
  dragImage: Element
) => { x: number; y: number };

export const DROP_EFFECTS = ['move', 'copy', 'link'] as DropEffect[];

export const CUSTOM_MIME_TYPE = 'application/x-dnd';

function mimeTypeIsCustom(mimeType: string) {
  return mimeType.substring(0, CUSTOM_MIME_TYPE.length) === CUSTOM_MIME_TYPE;
}

export function getWellKnownMimeType(event: DragEvent): string | null {
  if (event.dataTransfer) {
    const types = event.dataTransfer.types;

    for (let i = 0; i < types.length; i++) {
      if (mimeTypeIsCustom(types[i])) {
        return types[i];
      }
    }
  }

  return null;
}

export function setDragData(
  event: DragEvent,
  data: DragDropData,
  effectAllowed: EffectAllowed
): void {
  const mimeType = CUSTOM_MIME_TYPE + (data.type ? '-' + data.type : '');

  const dataString = JSON.stringify(data);

  event.dataTransfer?.setData(mimeType, dataString);
}

export function getDropData(
  event: DragEvent,
  dragIsExternal: boolean
): DragDropData {
  const mimeType = getWellKnownMimeType(event);

  if (dragIsExternal === true) {
    if (mimeType !== null && mimeTypeIsCustom(mimeType)) {
      return JSON.parse(event.dataTransfer?.getData(mimeType) ?? '{}');
    }

    return {};
  }

  if (mimeType !== null) {
    return JSON.parse(event.dataTransfer?.getData(mimeType) ?? '{}');
  }

  return {};
}

export function filterEffects(
  effects: DropEffect[],
  allowed: EffectAllowed | DropEffect
): DropEffect[] {
  if (allowed === 'all' || allowed === 'uninitialized') {
    return effects;
  }

  return effects.filter(function (effect) {
    return allowed.toLowerCase().indexOf(effect) !== -1;
  });
}

export function getDirectChildElement(
  parentElement: Element,
  childElement: Element
): Element | null {
  let directChild: Node = childElement;

  while (directChild.parentNode !== parentElement) {
    // reached root node without finding given parent
    if (!directChild.parentNode) {
      return null;
    }

    directChild = directChild.parentNode;
  }

  return directChild as Element;
}

export function shouldPositionPlaceholderBeforeElement(
  event: DragEvent,
  element: Element,
  horizontal: boolean
) {
  const bounds = element.getBoundingClientRect();

  // If the pointer is in the upper half of the list item element,
  // we position the placeholder before the list item, otherwise after it.
  if (horizontal) {
    return event.clientX < bounds.left + bounds.width / 2;
  }

  return event.clientY < bounds.top + bounds.height / 2;
}

export function calculateDragImageOffset(
  event: DragEvent,
  dragImage: Element
): { x: number; y: number } {
  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;

  return {
    x: event.offsetX + paddingLeft + borderLeft,
    y: event.offsetY + paddingTop + borderTop,
  };
}

export function setDragImage(
  event: DragEvent,
  dragImage: Element,
  offsetFunction: DndDragImageOffsetFunction
): void {
  const offset = offsetFunction(event, dragImage) || { x: 0, y: 0 };

  (event.dataTransfer as any).setDragImage(dragImage, offset.x, offset.y);
}


================================================
FILE: projects/dnd/src/lib/dnd.module.ts
================================================
import { NgModule } from '@angular/core';
import {
  DndDraggableDirective,
  DndDragImageRefDirective,
} from './dnd-draggable.directive';
import {
  DndDropzoneDirective,
  DndPlaceholderRefDirective,
} from './dnd-dropzone.directive';
import { DndHandleDirective } from './dnd-handle.directive';

@NgModule({
  exports: [
    DndDraggableDirective,
    DndDropzoneDirective,
    DndHandleDirective,
    DndPlaceholderRefDirective,
    DndDragImageRefDirective,
  ],
  imports: [
    DndDragImageRefDirective,
    DndDropzoneDirective,
    DndHandleDirective,
    DndPlaceholderRefDirective,
    DndDraggableDirective,
  ],
})
export class DndModule {}


================================================
FILE: projects/dnd/src/public-api.ts
================================================
/*
 * Public API Surface of dnd
 */

export * from './lib/dnd-draggable.directive';
export * from './lib/dnd-dropzone.directive';
export * from './lib/dnd-handle.directive';
export * from './lib/dnd-types';
export { DndDragImageOffsetFunction } from './lib/dnd-utils';
export * from './lib/dnd.module';


================================================
FILE: projects/dnd/src/test-setup.ts
================================================
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';

// jsdom does not implement DragEvent
if (typeof globalThis.DragEvent === 'undefined') {
  (globalThis as any).DragEvent = class DragEvent extends MouseEvent {
    public dataTransfer: DataTransfer | null;
    constructor(type: string, eventInitDict?: DragEventInit) {
      super(type, eventInitDict);
      this.dataTransfer = eventInitDict?.dataTransfer ?? null;
    }
  };
}

getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);


================================================
FILE: projects/dnd/tsconfig.lib.json
================================================
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "../../out-tsc/lib",
    "declaration": true,
    "declarationMap": true,
    "inlineSources": true,
    "types": []
  },
  "exclude": ["src/test.ts", "**/*.spec.ts"]
}


================================================
FILE: projects/dnd/tsconfig.lib.prod.json
================================================
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "extends": "./tsconfig.lib.json",
  "compilerOptions": {
    "declarationMap": false
  },
  "angularCompilerOptions": {
    "compilationMode": "partial"
  }
}


================================================
FILE: projects/dnd/tsconfig.spec.json
================================================
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "../../out-tsc/spec",
    "types": ["vitest/globals"]
  },
  "files": ["src/test-setup.ts"],
  "include": ["**/*.spec.ts", "**/*.d.ts"]
}


================================================
FILE: renovate.json
================================================
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:best-practices"]
}


================================================
FILE: tsconfig.json
================================================
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "paths": {
      "ngx-drag-drop": [
        "./dist/ngx-drag-drop/ngx-drag-drop",
        "./dist/ngx-drag-drop"
      ]
    },
    "declaration": false,
    "experimentalDecorators": true,
    "moduleResolution": "bundler",
    "importHelpers": true,
    "target": "es2022",
    "module": "es2022"
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}


================================================
FILE: vitest.config.ts
================================================
/// <reference types="vitest" />
import angular from '@analogjs/vite-plugin-angular';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [
    angular({
      tsconfig: 'projects/dnd/tsconfig.spec.json',
    }),
  ],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['projects/dnd/src/test-setup.ts'],
    include: ['projects/dnd/src/**/*.spec.ts'],
    coverage: {
      provider: 'v8',
      include: ['projects/dnd/src/lib/**/*.ts'],
      exclude: ['**/*.spec.ts'],
      reporter: ['text', 'json-summary', 'html'],
    },
  },
});
Download .txt
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
Download .txt
SYMBOL INDEX (133 symbols across 26 files)

FILE: projects/demo/src/app/app.component.ts
  constant TABS (line 7) | const TABS: string[] = [
  constant ISSUE_DEMOS (line 17) | const ISSUE_DEMOS: { issue: number; label: string }[] = [
  constant DEFAULT_TAB (line 21) | const DEFAULT_TAB = TABS[0];
  class AppComponent (line 29) | class AppComponent {
    method constructor (line 36) | constructor(
    method onTabLinkClick (line 61) | onTabLinkClick(tab: string) {
    method onIssueDemoClick (line 65) | onIssueDemoClick(issue: number) {

FILE: projects/demo/src/app/app.module.ts
  class AppRoutingModule (line 68) | class AppRoutingModule {}
  class AppModule (line 93) | class AppModule {}

FILE: projects/demo/src/app/demo-link/demo-link.component.ts
  class DemoLinkComponent (line 11) | class DemoLinkComponent {
    method url (line 15) | public get url(): string {

FILE: projects/demo/src/app/indirect-dnd-handle/indirect-dnd-handle.component.ts
  class IndirectDndHandleComponent (line 12) | class IndirectDndHandleComponent {
    method dragHandle (line 13) | get dragHandle() {

FILE: projects/demo/src/app/indirect-drag-image/indirect-drag-image.component.ts
  class IndirectDragImageComponent (line 11) | class IndirectDragImageComponent {}

FILE: projects/demo/src/app/issue-195/issue-195.component.ts
  class Issue195Component (line 22) | class Issue195Component {
    method onDrop (line 26) | onDrop(event: DndDropEvent) {

FILE: projects/demo/src/app/list/list.component.ts
  type DraggableItem (line 15) | interface DraggableItem {
  type DropzoneLayout (line 22) | interface DropzoneLayout {
  class ListComponent (line 42) | class ListComponent {
    method constructor (line 141) | constructor(private snackBarService: MatSnackBar) {}
    method setHorizontalLayout (line 143) | setHorizontalLayout(horizontalLayoutActive: boolean) {
    method onDragStart (line 149) | onDragStart(event: DragEvent) {
    method onDragged (line 157) | onDragged(item: any, list: any[], effect: DropEffect) {
    method onDragEnd (line 166) | onDragEnd(event: DragEvent) {
    method onDrop (line 176) | onDrop(event: DndDropEvent, list?: any[]) {

FILE: projects/demo/src/app/native/native.component.ts
  class NativeComponent (line 14) | class NativeComponent {
    method constructor (line 21) | constructor(private snackBarService: MatSnackBar) {}
    method onDrop (line 23) | onDrop(event: DndDropEvent) {

FILE: projects/demo/src/app/nested/nested.component.ts
  type NestableListItem (line 16) | interface NestableListItem {
  class NestedComponent (line 40) | class NestedComponent {
    method constructor (line 82) | constructor(private snackBarService: MatSnackBar) {}
    method onDragStart (line 84) | onDragStart(event: DragEvent) {
    method onDragged (line 92) | onDragged(item: any, list: any[], effect: DropEffect) {
    method onDragEnd (line 101) | onDragEnd(event: DragEvent) {
    method onDrop (line 111) | onDrop(event: DndDropEvent, list?: any[]) {

FILE: projects/demo/src/app/shadow-dom/shadow-dom.component.ts
  type NestableListItem (line 11) | interface NestableListItem {
  class ShadowDomComponent (line 29) | class ShadowDomComponent {
    method onDragged (line 55) | onDragged(
    method onDrop (line 66) | onDrop(event: DndDropEvent, list?: NestableListItem[]) {

FILE: projects/demo/src/app/simple/simple.component.ts
  type DraggableItem (line 19) | interface DraggableItem {
  class SimpleComponent (line 44) | class SimpleComponent {
    method constructor (line 91) | constructor(private snackBarService: MatSnackBar) {}
    method onDragStart (line 111) | onDragStart(event: DragEvent) {
    method onDragged (line 121) | onDragged($event: DragEvent, effect: string) {
    method onDragEnd (line 125) | onDragEnd(event: DragEvent) {
    method onDrop (line 135) | onDrop(event: DndDropEvent) {

FILE: projects/demo/src/app/tree/tree.component.ts
  type DraggableItem (line 13) | interface DraggableItem {
  class TreeComponent (line 32) | class TreeComponent {
    method onDragged (line 102) | onDragged(item: any, list: any[], effect: DropEffect) {
    method onDrop (line 109) | onDrop(event: DndDropEvent, list?: any[]) {

FILE: projects/demo/src/app/typed/typed.component.ts
  type Apple (line 10) | type Apple = 'apple';
  type Banana (line 11) | type Banana = 'banana';
  type FruitType (line 12) | type FruitType = Apple | Banana;
  type Fruit (line 14) | interface Fruit {
  function createFruit (line 21) | function createFruit(type: FruitType) {
  function range (line 28) | function range(start: number, end: number) {
  class TypedComponent (line 44) | class TypedComponent {
    method trackByFruit (line 58) | trackByFruit(index: number, fruit: Fruit) {
    method onDragged (line 62) | onDragged(index: number, fruit: Fruit, list: Fruit[]) {
    method onDrop (line 70) | onDrop(event: DndDropEvent, list: Fruit[]) {

FILE: projects/dnd/src/lib/dnd-draggable.directive.spec.ts
  class BasicDraggableHost (line 12) | @Component({
  class DisabledDraggableHost (line 21) | @Component({
  class HandleDraggableHost (line 28) | @Component({
  class DragImageHost (line 40) | @Component({

FILE: projects/dnd/src/lib/dnd-draggable.directive.ts
  class DndDragImageRefDirective (line 29) | class DndDragImageRefDirective implements OnInit {
    method ngOnInit (line 33) | ngOnInit() {
  class DndDraggableDirective (line 39) | class DndDraggableDirective implements AfterViewInit, OnDestroy {
    method dndDisableIf (line 75) | set dndDisableIf(value: boolean) {
    method dndDisableDragIf (line 91) | set dndDisableDragIf(value: boolean) {
    method ngAfterViewInit (line 95) | ngAfterViewInit(): void {
    method ngOnDestroy (line 104) | ngOnDestroy(): void {
    method onDragStart (line 114) | onDragStart(event: DndEvent): boolean {
    method onDrag (line 174) | onDrag(event: DragEvent) {
    method onDragEnd (line 178) | onDragEnd(event: DragEvent) {
    method registerDragHandle (line 228) | registerDragHandle(handle: DndHandleDirective | undefined) {
    method registerDragImage (line 232) | registerDragImage(elementRef: ElementRef | undefined) {
    method determineDragImage (line 240) | private determineDragImage(): Element {

FILE: projects/dnd/src/lib/dnd-dropzone.directive.spec.ts
  class BasicDropzoneHost (line 11) | @Component({
  class DisabledDropzoneHost (line 18) | @Component({
  class PlaceholderDropzoneHost (line 25) | @Component({
  class TypedDropzoneHost (line 37) | @Component({

FILE: projects/dnd/src/lib/dnd-dropzone.directive.ts
  type DndDropEvent (line 30) | interface DndDropEvent {
  class DndPlaceholderRefDirective (line 40) | class DndPlaceholderRefDirective implements OnInit {
    method constructor (line 41) | constructor(public readonly elementRef: ElementRef<HTMLElement>) {}
    method ngOnInit (line 43) | ngOnInit() {
  class DndDropzoneDirective (line 50) | class DndDropzoneDirective implements AfterViewInit, OnDestroy {
    method constructor (line 78) | constructor(
    method dndDisableIf (line 84) | set dndDisableIf(value: boolean) {
    method dndDisableDropIf (line 100) | set dndDisableDropIf(value: boolean) {
    method ngAfterViewInit (line 104) | ngAfterViewInit(): void {
    method ngOnDestroy (line 125) | ngOnDestroy(): void {
    method onDragEnter (line 140) | onDragEnter(event: DndEvent) {
    method onDragOver (line 170) | onDragOver(event: DragEvent) {
    method onDrop (line 209) | onDrop(event: DragEvent) {
    method onDragLeave (line 258) | onDragLeave(event: DndEvent) {
    method isDropAllowed (line 284) | private isDropAllowed(type?: string): boolean {
    method tryGetPlaceholder (line 316) | private tryGetPlaceholder(): Element | null {
    method removePlaceholderFromDOM (line 326) | private removePlaceholderFromDOM() {
    method checkAndUpdatePlaceholderPosition (line 332) | private checkAndUpdatePlaceholderPosition(event: DragEvent): void {
    method getPlaceholderIndex (line 384) | private getPlaceholderIndex(): number | undefined {
    method cleanupDragoverState (line 394) | private cleanupDragoverState() {

FILE: projects/dnd/src/lib/dnd-handle.directive.spec.ts
  class HandleHost (line 9) | @Component({

FILE: projects/dnd/src/lib/dnd-handle.directive.ts
  class DndHandleDirective (line 13) | class DndHandleDirective implements OnInit, OnDestroy {
    method ngOnInit (line 18) | ngOnInit() {
    method ngOnDestroy (line 22) | ngOnDestroy(): void {
    method onDragEvent (line 28) | onDragEvent(event: DndEvent) {

FILE: projects/dnd/src/lib/dnd-state.spec.ts
  function createMockDragEvent (line 13) | function createMockDragEvent(overrides: Partial<DragEvent> = {}): DragEv...

FILE: projects/dnd/src/lib/dnd-state.ts
  type DndState (line 9) | interface DndState {
  function startDrag (line 23) | function startDrag(
  function endDrag (line 38) | function endDrag() {
  function setDropEffect (line 45) | function setDropEffect(event: DragEvent, dropEffect: DropEffect) {
  function getDropEffect (line 55) | function getDropEffect(
  function getDndType (line 90) | function getDndType(event: DragEvent): string | undefined {
  function isExternalDrag (line 104) | function isExternalDrag(): boolean {

FILE: projects/dnd/src/lib/dnd-types.ts
  type DropEffect (line 2) | type DropEffect = 'move' | 'copy' | 'link' | 'none';
  type EffectAllowed (line 5) | type EffectAllowed =

FILE: projects/dnd/src/lib/dnd-utils.spec.ts
  function createMockDragEvent (line 14) | function createMockDragEvent(overrides: Partial<DragEvent> = {}): DragEv...

FILE: projects/dnd/src/lib/dnd-utils.ts
  type DragDropData (line 3) | interface DragDropData {
  type DndEvent (line 8) | interface DndEvent extends DragEvent {
  type DndDragImageOffsetFunction (line 13) | type DndDragImageOffsetFunction = (
  constant DROP_EFFECTS (line 18) | const DROP_EFFECTS = ['move', 'copy', 'link'] as DropEffect[];
  constant CUSTOM_MIME_TYPE (line 20) | const CUSTOM_MIME_TYPE = 'application/x-dnd';
  function mimeTypeIsCustom (line 22) | function mimeTypeIsCustom(mimeType: string) {
  function getWellKnownMimeType (line 26) | function getWellKnownMimeType(event: DragEvent): string | null {
  function setDragData (line 40) | function setDragData(
  function getDropData (line 52) | function getDropData(
  function filterEffects (line 73) | function filterEffects(
  function getDirectChildElement (line 86) | function getDirectChildElement(
  function shouldPositionPlaceholderBeforeElement (line 104) | function shouldPositionPlaceholderBeforeElement(
  function calculateDragImageOffset (line 120) | function calculateDragImageOffset(
  function setDragImage (line 136) | function setDragImage(

FILE: projects/dnd/src/lib/dnd.module.ts
  class DndModule (line 28) | class DndModule {}

FILE: projects/dnd/src/test-setup.ts
  method constructor (line 13) | constructor(type: string, eventInitDict?: DragEventInit) {
Condensed preview — 89 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (164K chars).
[
  {
    "path": ".editorconfig",
    "chars": 274,
    "preview": "# Editor configuration, see https://editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size ="
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Bug_report.md",
    "chars": 958,
    "preview": "---\r\nname: Bug report\r\nabout: Create a report to help us improve\r\n\r\n---\r\n\r\n**Describe the bug**\r\nA clear and concise des"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Feature_request.md",
    "chars": 577,
    "preview": "---\r\nname: Feature request\r\nabout: Suggest an idea for this project\r\n\r\n---\r\n\r\n**Is your feature request related to a pro"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1625,
    "preview": "name: CI\n\non:\n  pull_request:\n    branches: [master]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: "
  },
  {
    "path": ".github/workflows/deploy-pages.yml",
    "chars": 1061,
    "preview": "name: Deploy Demo to GitHub Pages\n\non:\n  push:\n    branches: [master]\n  workflow_dispatch:\n\npermissions:\n  contents: rea"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 1481,
    "preview": "name: Publish to npm\n\non:\n  release:\n    types: [published]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      -"
  },
  {
    "path": ".gitignore",
    "chars": 684,
    "preview": "# See http://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist\n/docs\n/tmp\n/out-tsc\n#"
  },
  {
    "path": ".prettierignore",
    "chars": 690,
    "preview": "# See http://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist\n/docs\n/tmp\n/out-tsc\n#"
  },
  {
    "path": ".prettierrc.json",
    "chars": 204,
    "preview": "{\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"singleQuote\": true,\n  \"semi\": true,\n  \"bracketSpacing\": true,\n  \"arrowParens\":"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 130,
    "preview": "{\n  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846\n  \"recommendations\": [\"angular.ng-tem"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 474,
    "preview": "{\n  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n  \"version\": \"0.2.0\",\n  \"configuratio"
  },
  {
    "path": ".vscode/tasks.json",
    "chars": 938,
    "preview": "{\n  // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n  "
  },
  {
    "path": "LICENSE",
    "chars": 1516,
    "preview": "BSD 3-Clause License\n\nCopyright (c) 2021, Stefan Steinhart\nAll rights reserved.\n\nRedistribution and use in source and bi"
  },
  {
    "path": "README.md",
    "chars": 11198,
    "preview": "[![npm](https://img.shields.io/npm/v/ngx-drag-drop.svg)](https://www.npmjs.com/package/ngx-drag-drop)\n[![npm (next)](htt"
  },
  {
    "path": "angular.json",
    "chars": 4087,
    "preview": "{\n  \"$schema\": \"./node_modules/@angular/cli/lib/config/schema.json\",\n  \"cli\": {\n    \"analytics\": false\n  },\n  \"version\":"
  },
  {
    "path": "package.json",
    "chars": 1725,
    "preview": "{\n  \"name\": \"ngx-drag-drop-workspace\",\n  \"version\": \"0.0.0\",\n  \"scripts\": {\n    \"ng\": \"ng\",\n    \"start\": \"pnpm run build"
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 93,
    "preview": "allowBuilds:\n  '@parcel/watcher': true\n  esbuild: true\n  lmdb: true\n  msgpackr-extract: true\n"
  },
  {
    "path": "projects/demo/.browserslistrc",
    "chars": 600,
    "preview": "# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.\n# For addit"
  },
  {
    "path": "projects/demo/src/app/app.component.html",
    "chars": 1131,
    "preview": "<mat-toolbar class=\"mat-elevation-z4 flex-shrink-0\" color=\"primary\">\n  <span>{{ title }}</span>\n\n  <span class=\"flex-gro"
  },
  {
    "path": "projects/demo/src/app/app.component.scss",
    "chars": 752,
    "preview": ":host {\n  display: flex;\n  flex: 1 1 auto;\n  flex-direction: column;\n  max-height: 100vh;\n  max-width: 100vw;\n  height: "
  },
  {
    "path": "projects/demo/src/app/app.component.ts",
    "chars": 1746,
    "preview": "import { Component } from '@angular/core';\nimport { MatIconRegistry } from '@angular/material/icon';\nimport { DomSanitiz"
  },
  {
    "path": "projects/demo/src/app/app.module.ts",
    "chars": 2607,
    "preview": "import {\n  provideHttpClient,\n  withInterceptorsFromDi,\n} from '@angular/common/http';\nimport { NgModule } from '@angula"
  },
  {
    "path": "projects/demo/src/app/demo-link/demo-link.component.html",
    "chars": 67,
    "preview": "<a [href]=\"url\" color=\"accent\" mat-raised-button>go to sources</a>\n"
  },
  {
    "path": "projects/demo/src/app/demo-link/demo-link.component.scss",
    "chars": 27,
    "preview": ":host {\n  margin: 8px 0;\n}\n"
  },
  {
    "path": "projects/demo/src/app/demo-link/demo-link.component.ts",
    "chars": 514,
    "preview": "import { Component, Input } from '@angular/core';\nimport { MatButtonModule } from '@angular/material/button';\n\n@Componen"
  },
  {
    "path": "projects/demo/src/app/indirect-dnd-handle/indirect-dnd-handle.component.html",
    "chars": 55,
    "preview": "<mat-icon dndHandle fontIcon=\"drag_handle\"></mat-icon>\n"
  },
  {
    "path": "projects/demo/src/app/indirect-dnd-handle/indirect-dnd-handle.component.scss",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "projects/demo/src/app/indirect-dnd-handle/indirect-dnd-handle.component.ts",
    "chars": 514,
    "preview": "import { Component, HostBinding } from '@angular/core';\nimport { MatIconModule } from '@angular/material/icon';\nimport {"
  },
  {
    "path": "projects/demo/src/app/indirect-drag-image/indirect-drag-image.component.html",
    "chars": 57,
    "preview": "<div dndDragImageRef>\n  <ng-content></ng-content>\n</div>\n"
  },
  {
    "path": "projects/demo/src/app/indirect-drag-image/indirect-drag-image.component.scss",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "projects/demo/src/app/indirect-drag-image/indirect-drag-image.component.ts",
    "chars": 369,
    "preview": "import { Component } from '@angular/core';\nimport { DndDragImageRefDirective } from 'ngx-drag-drop';\n\n@Component({\n  sel"
  },
  {
    "path": "projects/demo/src/app/issue-195/issue-195.component.html",
    "chars": 1521,
    "preview": "<h3>Issue #195: dropEffect ignores dropzone's dndEffectAllowed</h3>\n<p>\n  Draggable has <code>dndEffectAllowed=\"all\"</co"
  },
  {
    "path": "projects/demo/src/app/issue-195/issue-195.component.scss",
    "chars": 686,
    "preview": ".demo {\n  display: flex;\n  gap: 32px;\n  padding: 16px;\n  align-items: flex-start;\n}\n\n.draggable {\n  padding: 16px 24px;\n"
  },
  {
    "path": "projects/demo/src/app/issue-195/issue-195.component.ts",
    "chars": 672,
    "preview": "import { JsonPipe } from '@angular/common';\nimport { Component } from '@angular/core';\nimport {\n  DndDraggableDirective,"
  },
  {
    "path": "projects/demo/src/app/list/list.component.html",
    "chars": 3968,
    "preview": "<div class=\"container-fluid\">\n  <div class=\"row\">\n    <div class=\"col\">\n      <pre>dndEffectAllowed=\"copyMove\"</pre>\n\n  "
  },
  {
    "path": "projects/demo/src/app/list/list.component.scss",
    "chars": 555,
    "preview": ":host {\n  display: block;\n}\n\npre {\n  text-align: center;\n  padding: 8px;\n}\n\n.dndList {\n  transition: all 300ms ease;\n  p"
  },
  {
    "path": "projects/demo/src/app/list/list.component.ts",
    "chars": 4150,
    "preview": "import { Component } from '@angular/core';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatListModul"
  },
  {
    "path": "projects/demo/src/app/native/native.component.html",
    "chars": 2795,
    "preview": "<div class=\"container-fluid\">\n  <div class=\"row\">\n    <div class=\"col\">\n      <mat-card appearance=\"raised\">\n        <ma"
  },
  {
    "path": "projects/demo/src/app/native/native.component.scss",
    "chars": 113,
    "preview": ":host {\n  display: block;\n  box-sizing: border-box;\n}\n\npre {\n  white-space: pre-wrap;\n  word-wrap: break-word;\n}\n"
  },
  {
    "path": "projects/demo/src/app/native/native.component.ts",
    "chars": 1898,
    "preview": "import { JsonPipe } from '@angular/common';\nimport { Component } from '@angular/core';\nimport { MatCardModule } from '@a"
  },
  {
    "path": "projects/demo/src/app/nested/nested.component.html",
    "chars": 1855,
    "preview": "<div class=\"container-fluid\">\n  <ng-template #recursiveList let-list>\n    <mat-card appearance=\"raised\" class=\"dndPlaceh"
  },
  {
    "path": "projects/demo/src/app/nested/nested.component.scss",
    "chars": 501,
    "preview": ":host {\n  display: block;\n  box-sizing: border-box;\n}\n\nmat-card {\n  transition:\n    transform 200ms,\n    opacity 200ms;\n"
  },
  {
    "path": "projects/demo/src/app/nested/nested.component.ts",
    "chars": 2855,
    "preview": "import { NgTemplateOutlet } from '@angular/common';\nimport { Component } from '@angular/core';\nimport { MatCardModule } "
  },
  {
    "path": "projects/demo/src/app/shadow-dom/shadow-dom.component.html",
    "chars": 914,
    "preview": "<ng-template #recursiveList let-list>\n  <div class=\"placeholder\" dndPlaceholderRef></div>\n\n  @for (item of list; track i"
  },
  {
    "path": "projects/demo/src/app/shadow-dom/shadow-dom.component.scss",
    "chars": 629,
    "preview": ":host {\n  display: block;\n  padding: 16px;\n}\n\n.root-dropzone,\n.nested-dropzone {\n  display: flex;\n  flex-direction: colu"
  },
  {
    "path": "projects/demo/src/app/shadow-dom/shadow-dom.component.ts",
    "chars": 1764,
    "preview": "import { NgTemplateOutlet } from '@angular/common';\nimport { Component, ViewEncapsulation } from '@angular/core';\nimport"
  },
  {
    "path": "projects/demo/src/app/simple/simple.component.html",
    "chars": 4756,
    "preview": "<div class=\"container-fluid\">\n  <div class=\"row gap-3 gap-sm-0\">\n    <div class=\"d-flex flex-column col gap-2\">\n      @f"
  },
  {
    "path": "projects/demo/src/app/simple/simple.component.scss",
    "chars": 215,
    "preview": ":host {\n  display: block;\n  height: 100%;\n}\n\n.my-dropzone {\n  transition: all 300ms ease;\n}\n\n.custom-drag-over {\n  backg"
  },
  {
    "path": "projects/demo/src/app/simple/simple.component.ts",
    "chars": 3642,
    "preview": "import { JsonPipe } from '@angular/common';\nimport { Component } from '@angular/core';\nimport { MatCardModule } from '@a"
  },
  {
    "path": "projects/demo/src/app/tree/tree.component.html",
    "chars": 1510,
    "preview": "<div class=\"container-fluid\">\n  <ng-template #recursiveList let-list>\n    <mat-list-item\n      class=\"dndPlaceholder bor"
  },
  {
    "path": "projects/demo/src/app/tree/tree.component.scss",
    "chars": 130,
    "preview": ".dndDraggingSource {\n  opacity: 0.5;\n  transform: scale(0.98);\n  pointer-events: none;\n\n  & > * {\n    pointer-events: no"
  },
  {
    "path": "projects/demo/src/app/tree/tree.component.ts",
    "chars": 2352,
    "preview": "import { CommonModule } from '@angular/common';\nimport { Component } from '@angular/core';\nimport { MatIconModule } from"
  },
  {
    "path": "projects/demo/src/app/typed/typed.component.html",
    "chars": 2691,
    "preview": "<div class=\"container-fluid\">\n  <div class=\"row\">\n    <div class=\"col\">\n      <pre>Fruits</pre>\n      <mat-list\n        "
  },
  {
    "path": "projects/demo/src/app/typed/typed.component.scss",
    "chars": 602,
    "preview": ":host {\n  display: block;\n  box-sizing: border-box;\n}\n\npre {\n  text-align: center;\n  padding: 8px;\n}\n\n.mat-mdc-list-item"
  },
  {
    "path": "projects/demo/src/app/typed/typed.component.ts",
    "chars": 1821,
    "preview": "import { Component } from '@angular/core';\nimport { MatListModule } from '@angular/material/list';\nimport {\n  DndDraggab"
  },
  {
    "path": "projects/demo/src/assets/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "projects/demo/src/dragdroptouch.d.ts",
    "chars": 188,
    "preview": "declare module '@dragdroptouch/drag-drop-touch' {\n  export function enableDragDropTouch(\n    dragRoot?: Element,\n    dro"
  },
  {
    "path": "projects/demo/src/environments/environment.prod.ts",
    "chars": 52,
    "preview": "export const environment = {\n  production: true,\n};\n"
  },
  {
    "path": "projects/demo/src/environments/environment.ts",
    "chars": 659,
    "preview": "// This file can be replaced during build by using the `fileReplacements` array.\n// `ng build` replaces `environment.ts`"
  },
  {
    "path": "projects/demo/src/index.html",
    "chars": 643,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>NgxDragDrop</title>\n    <base href=\"/\""
  },
  {
    "path": "projects/demo/src/main.ts",
    "chars": 446,
    "preview": "import { enableProdMode, provideZoneChangeDetection } from '@angular/core';\nimport { platformBrowser } from '@angular/pl"
  },
  {
    "path": "projects/demo/src/polyfills.ts",
    "chars": 2429,
    "preview": "/**\n * This file includes polyfills needed by Angular and is loaded before the app.\n * You can add your own extra polyfi"
  },
  {
    "path": "projects/demo/src/styles.scss",
    "chars": 1202,
    "preview": "@import 'bootstrap/scss/bootstrap.scss';\n\n.drag-handle {\n  border: 1px solid #ddd;\n  border-radius: 50%;\n  display: inli"
  },
  {
    "path": "projects/demo/tsconfig.app.json",
    "chars": 275,
    "preview": "/* To learn more about this file see: https://angular.io/config/tsconfig. */\n{\n  \"extends\": \"../../tsconfig.json\",\n  \"co"
  },
  {
    "path": "projects/demo/tsconfig.spec.json",
    "chars": 279,
    "preview": "/* To learn more about this file see: https://angular.io/config/tsconfig. */\n{\n  \"extends\": \"../../tsconfig.json\",\n  \"co"
  },
  {
    "path": "projects/dnd/.browserslistrc",
    "chars": 600,
    "preview": "# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.\n# For addit"
  },
  {
    "path": "projects/dnd/README.md",
    "chars": 10290,
    "preview": "[![npm](https://img.shields.io/npm/v/ngx-drag-drop.svg)](https://www.npmjs.com/package/ngx-drag-drop)\n[![npm (next)](htt"
  },
  {
    "path": "projects/dnd/ng-package.json",
    "chars": 163,
    "preview": "{\n  \"$schema\": \"../../node_modules/ng-packagr/ng-package.schema.json\",\n  \"dest\": \"../../dist/ngx-drag-drop\",\n  \"lib\": {\n"
  },
  {
    "path": "projects/dnd/package.json",
    "chars": 699,
    "preview": "{\n  \"name\": \"ngx-drag-drop\",\n  \"version\": \"21.0.6\",\n  \"description\": \"Angular directives using the native HTML Drag And "
  },
  {
    "path": "projects/dnd/src/lib/dnd-draggable.directive.spec.ts",
    "chars": 4818,
    "preview": "import { Component, DebugElement } from '@angular/core';\nimport { ComponentFixture, TestBed } from '@angular/core/testin"
  },
  {
    "path": "projects/dnd/src/lib/dnd-draggable.directive.ts",
    "chars": 6770,
    "preview": "import {\n  AfterViewInit,\n  Directive,\n  ElementRef,\n  EventEmitter,\n  forwardRef,\n  HostBinding,\n  HostListener,\n  inje"
  },
  {
    "path": "projects/dnd/src/lib/dnd-dropzone.directive.spec.ts",
    "chars": 4552,
    "preview": "import { Component, DebugElement } from '@angular/core';\nimport { ComponentFixture, TestBed } from '@angular/core/testin"
  },
  {
    "path": "projects/dnd/src/lib/dnd-dropzone.directive.ts",
    "chars": 10650,
    "preview": "import {\n  AfterViewInit,\n  ContentChild,\n  Directive,\n  ElementRef,\n  EventEmitter,\n  HostListener,\n  Input,\n  NgZone,\n"
  },
  {
    "path": "projects/dnd/src/lib/dnd-handle.directive.spec.ts",
    "chars": 2059,
    "preview": "import { Component, DebugElement } from '@angular/core';\nimport { ComponentFixture, TestBed } from '@angular/core/testin"
  },
  {
    "path": "projects/dnd/src/lib/dnd-handle.directive.ts",
    "chars": 770,
    "preview": "import {\n  Directive,\n  HostBinding,\n  HostListener,\n  inject,\n  OnDestroy,\n  OnInit,\n} from '@angular/core';\nimport { D"
  },
  {
    "path": "projects/dnd/src/lib/dnd-state.spec.ts",
    "chars": 5343,
    "preview": "import { describe, it, expect, beforeEach, vi } from 'vitest';\nimport {\n  startDrag,\n  endDrag,\n  setDropEffect,\n  getDr"
  },
  {
    "path": "projects/dnd/src/lib/dnd-state.ts",
    "chars": 2593,
    "preview": "import { DropEffect, EffectAllowed } from './dnd-types';\nimport {\n  CUSTOM_MIME_TYPE,\n  DROP_EFFECTS,\n  filterEffects,\n "
  },
  {
    "path": "projects/dnd/src/lib/dnd-types.ts",
    "chars": 335,
    "preview": "// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/dropEffect\nexport type DropEffect = 'move' | 'copy' | '"
  },
  {
    "path": "projects/dnd/src/lib/dnd-utils.spec.ts",
    "chars": 8683,
    "preview": "import { describe, it, expect, vi } from 'vitest';\nimport {\n  filterEffects,\n  getWellKnownMimeType,\n  setDragData,\n  ge"
  },
  {
    "path": "projects/dnd/src/lib/dnd-utils.ts",
    "chars": 3668,
    "preview": "import { DropEffect, EffectAllowed } from './dnd-types';\n\nexport interface DragDropData {\n  data?: any;\n  type?: string;"
  },
  {
    "path": "projects/dnd/src/lib/dnd.module.ts",
    "chars": 655,
    "preview": "import { NgModule } from '@angular/core';\nimport {\n  DndDraggableDirective,\n  DndDragImageRefDirective,\n} from './dnd-dr"
  },
  {
    "path": "projects/dnd/src/public-api.ts",
    "chars": 303,
    "preview": "/*\n * Public API Surface of dnd\n */\n\nexport * from './lib/dnd-draggable.directive';\nexport * from './lib/dnd-dropzone.di"
  },
  {
    "path": "projects/dnd/src/test-setup.ts",
    "chars": 704,
    "preview": "import 'zone.js';\nimport 'zone.js/testing';\nimport { getTestBed } from '@angular/core/testing';\nimport {\n  BrowserDynami"
  },
  {
    "path": "projects/dnd/tsconfig.lib.json",
    "chars": 321,
    "preview": "/* To learn more about this file see: https://angular.io/config/tsconfig. */\n{\n  \"extends\": \"../../tsconfig.json\",\n  \"co"
  },
  {
    "path": "projects/dnd/tsconfig.lib.prod.json",
    "chars": 240,
    "preview": "/* To learn more about this file see: https://angular.io/config/tsconfig. */\n{\n  \"extends\": \"./tsconfig.lib.json\",\n  \"co"
  },
  {
    "path": "projects/dnd/tsconfig.spec.json",
    "chars": 213,
    "preview": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../out-tsc/spec\",\n    \"types\": [\"vitest/g"
  },
  {
    "path": "renovate.json",
    "chars": 109,
    "preview": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\"config:best-practices\"]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "chars": 926,
    "preview": "/* To learn more about this file see: https://angular.io/config/tsconfig. */\n{\n  \"compileOnSave\": false,\n  \"compilerOpti"
  },
  {
    "path": "vitest.config.ts",
    "chars": 595,
    "preview": "/// <reference types=\"vitest\" />\nimport angular from '@analogjs/vite-plugin-angular';\nimport { defineConfig } from 'vite"
  }
]

About this extraction

This page contains the full source code of the reppners/ngx-drag-drop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 89 files (147.4 KB), approximately 41.6k tokens, and a symbol index with 133 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!