├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── projects ├── demo │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── app │ │ │ ├── app.component.ts │ │ │ └── app.config.ts │ │ ├── index.html │ │ ├── main.ts │ │ └── styles.css │ └── tsconfig.app.json └── ngx-file-drag-drop │ ├── README.md │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── byte.pipe.ts │ │ ├── ngx-file-drag-drop.component.ts │ │ └── validators.ts │ └── public-api.ts │ ├── tsconfig.lib.json │ └── tsconfig.lib.prod.json ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Telebroad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx-file-drag-drop 2 | 3 | #### Check out the [Demo](https://stackblitz.com/edit/ngx-file-drag-drop) 4 | 5 | ## Install 6 | 7 | ``` 8 | npm i ngx-file-drag-drop 9 | ``` 10 | 11 | ### NgxFileDragDropComponent 12 | 13 | selector: `` 14 | 15 | implements: [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor) to work with angular forms 16 | 17 | **Additionnal properties** 18 | 19 | | Name | Description | 20 | | -------------------------------------------------------- | ------------------------------------------------------------------------ | 21 | | _@Input()_ multiple: `boolean` | Allows multiple file inputs, `false` by default | 22 | | _@Input()_ accept: `string` | Any value the native `accept` attribute can get. Doesn't validate input. | 23 | | _@Input()_ disabled: `boolean` | Disable the input. | 24 | | _@Input()_ emptyPlaceholder : `string` | Placeholder for empty input, default `Drop file or click to select` | 25 | | _@Input()_ displayFileSize : `boolean` | Show file size in chip rather than in tooltip, default `false` | 26 | | _@Input()_ activeBorderColor: `string` | A css color for when file is dragged into drop area, default `purple` | 27 | | _@Output()_ valueChanged:`EventEmitter` | Event emitted when input value changes | 28 | | addFiles():`(files: File[] \| FileList \| File) => void` | Update input | 29 | | removeFile():`(file:File) => void` | Removes the file from the input | 30 | | clear(): `() => void` | Removes all files from the input | 31 | | files: `File[]` | Getter for form value | 32 | | isEmpty: `boolean` | Whether the input is empty (no files) or not | 33 | 34 | ### BytePipe 35 | 36 | Usage: 37 | 38 | ```html 39 | {{ 104857600 | byteFormat }} 40 | ``` 41 | 42 | _Output:_ 100 MB 43 | 44 | ### Validators 45 | 46 | ```ts 47 | import { FileValidators } from "ngx-file-drag-drop"; 48 | ``` 49 | 50 | | Validator | Description | 51 | | ------------------------------------- | -------------------------------------- | 52 | | `uniqueFileNames` | Disallow two files with same file name | 53 | | `fileExtension(ext: string[])` | Required file extensions | 54 | | `fileType(types: string[] \| RegExp)` | Required Mime Types | 55 | | `maxFileCount(count: number)` | Max number of files | 56 | | `maxFileSize(bytes: number)` | Max bytes allowed per file | 57 | | `maxTotalSize(bytes: number)` | Max total input size | 58 | | `required` | At least one file required | 59 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-file-drag-drop": { 7 | "projectType": "library", 8 | "root": "projects/ngx-file-drag-drop", 9 | "sourceRoot": "projects/ngx-file-drag-drop/src", 10 | "prefix": "ngx", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "project": "projects/ngx-file-drag-drop/ng-package.json" 16 | }, 17 | "configurations": { 18 | "production": { 19 | "tsConfig": "projects/ngx-file-drag-drop/tsconfig.lib.prod.json" 20 | }, 21 | "development": { 22 | "tsConfig": "projects/ngx-file-drag-drop/tsconfig.lib.json" 23 | } 24 | }, 25 | "defaultConfiguration": "production" 26 | } 27 | } 28 | }, 29 | "demo": { 30 | "projectType": "application", 31 | "schematics": { 32 | "@schematics/angular:component": { 33 | "inlineTemplate": true, 34 | "inlineStyle": true, 35 | "skipTests": true 36 | }, 37 | "@schematics/angular:class": { 38 | "skipTests": true 39 | }, 40 | "@schematics/angular:directive": { 41 | "skipTests": true 42 | }, 43 | "@schematics/angular:guard": { 44 | "skipTests": true 45 | }, 46 | "@schematics/angular:interceptor": { 47 | "skipTests": true 48 | }, 49 | "@schematics/angular:pipe": { 50 | "skipTests": true 51 | }, 52 | "@schematics/angular:resolver": { 53 | "skipTests": true 54 | }, 55 | "@schematics/angular:service": { 56 | "skipTests": true 57 | } 58 | }, 59 | "root": "projects/demo", 60 | "sourceRoot": "projects/demo/src", 61 | "prefix": "app", 62 | "architect": { 63 | "build": { 64 | "builder": "@angular-devkit/build-angular:application", 65 | "options": { 66 | "outputPath": "dist/demo", 67 | "index": "projects/demo/src/index.html", 68 | "browser": "projects/demo/src/main.ts", 69 | "polyfills": ["zone.js"], 70 | "tsConfig": "projects/demo/tsconfig.app.json", 71 | "assets": [ 72 | { 73 | "glob": "**/*", 74 | "input": "projects/demo/public" 75 | } 76 | ], 77 | "styles": [ 78 | "@angular/material/prebuilt-themes/azure-blue.css", 79 | "projects/demo/src/styles.css" 80 | ], 81 | "scripts": [] 82 | }, 83 | "configurations": { 84 | "production": { 85 | "budgets": [ 86 | { 87 | "type": "initial", 88 | "maximumWarning": "500kB", 89 | "maximumError": "1MB" 90 | }, 91 | { 92 | "type": "anyComponentStyle", 93 | "maximumWarning": "2kB", 94 | "maximumError": "4kB" 95 | } 96 | ], 97 | "outputHashing": "all" 98 | }, 99 | "development": { 100 | "optimization": false, 101 | "extractLicenses": false, 102 | "sourceMap": true 103 | } 104 | }, 105 | "defaultConfiguration": "production" 106 | }, 107 | "serve": { 108 | "builder": "@angular-devkit/build-angular:dev-server", 109 | "configurations": { 110 | "production": { 111 | "buildTarget": "demo:build:production" 112 | }, 113 | "development": { 114 | "buildTarget": "demo:build:development" 115 | } 116 | }, 117 | "defaultConfiguration": "development" 118 | }, 119 | "extract-i18n": { 120 | "builder": "@angular-devkit/build-angular:extract-i18n" 121 | } 122 | } 123 | } 124 | }, 125 | "cli": { 126 | "schematicCollections": ["@angular-eslint/schematics"] 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-file-drag-drop", 3 | "version": "11.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development" 9 | }, 10 | "private": true, 11 | "dependencies": { 12 | "@angular/animations": "^19.0.5", 13 | "@angular/cdk": "^19.0.4", 14 | "@angular/common": "^19.0.5", 15 | "@angular/compiler": "^19.0.5", 16 | "@angular/core": "^19.0.5", 17 | "@angular/forms": "^19.0.5", 18 | "@angular/material": "^19.0.4", 19 | "@angular/platform-browser": "^19.0.5", 20 | "@angular/platform-browser-dynamic": "^19.0.5", 21 | "@angular/router": "^19.0.5", 22 | "rxjs": "~7.8.0", 23 | "tslib": "^2.3.0", 24 | "zone.js": "~0.15.0" 25 | }, 26 | "devDependencies": { 27 | "@angular-devkit/build-angular": "^19.0.6", 28 | "@angular/cli": "^19.0.6", 29 | "@angular/compiler-cli": "^19.0.5", 30 | "ng-packagr": "^19.0.1", 31 | "typescript": "~5.6.3" 32 | } 33 | } -------------------------------------------------------------------------------- /projects/demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/telebroad/ngx-file-drag-drop/d902640f0ad74e9c8f679a199cb6213be0d7de02/projects/demo/public/favicon.ico -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NgxFileDragDropComponent } from '../../../ngx-file-drag-drop/src/public-api'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | standalone: true, 7 | imports: [NgxFileDragDropComponent], 8 | template: ` 9 | 10 | `, 11 | styles: [], 12 | }) 13 | export class AppComponent { 14 | title = 'demo'; 15 | } 16 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; 2 | import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; 3 | 4 | 5 | export const appConfig: ApplicationConfig = { 6 | providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideAnimationsAsync()] 7 | }; 8 | -------------------------------------------------------------------------------- /projects/demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /projects/demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /projects/demo/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, body { height: 100%; } 4 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 5 | -------------------------------------------------------------------------------- /projects/demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-file-drag-drop/README.md: -------------------------------------------------------------------------------- 1 | # ngx-file-drag-drop 2 | 3 | #### Check out the [Demo](https://stackblitz.com/edit/ngx-file-drag-drop) 4 | 5 | ## Install 6 | 7 | ``` 8 | npm i ngx-file-drag-drop 9 | ``` 10 | 11 | ### NgxFileDragDropComponent 12 | 13 | selector: `` 14 | 15 | implements: [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor) to work with angular forms 16 | 17 | **Additionnal properties** 18 | 19 | | Name | Description | 20 | | -------------------------------------------------------- | ------------------------------------------------------------------------ | 21 | | _@Input()_ multiple: `boolean` | Allows multiple file inputs, `false` by default | 22 | | _@Input()_ accept: `string` | Any value the native `accept` attribute can get. Doesn't validate input. | 23 | | _@Input()_ disabled: `boolean` | Disable the input. | 24 | | _@Input()_ emptyPlaceholder : `string` | Placeholder for empty input, default `Drop file or click to select` | 25 | | _@Input()_ displayFileSize : `boolean` | Show file size in chip rather than in tooltip, default `false` | 26 | | _@Input()_ activeBorderColor: `string` | A css color for when file is dragged into drop area, default `purple` | 27 | | _@Output()_ valueChanged:`EventEmitter` | Event emitted when input value changes | 28 | | addFiles():`(files: File[] \| FileList \| File) => void` | Update input | 29 | | removeFile():`(file:File) => void` | Removes the file from the input | 30 | | clear(): `() => void` | Removes all files from the input | 31 | | files: `File[]` | Getter for form value | 32 | | isEmpty: `boolean` | Whether the input is empty (no files) or not | 33 | 34 | ### BytePipe 35 | 36 | Usage: 37 | 38 | ```html 39 | {{ 104857600 | byteFormat }} 40 | ``` 41 | 42 | _Output:_ 100 MB 43 | 44 | ### Validators 45 | 46 | ```ts 47 | import { FileValidators } from "ngx-file-drag-drop"; 48 | ``` 49 | 50 | | Validator | Description | 51 | | ------------------------------------- | -------------------------------------- | 52 | | `uniqueFileNames` | Disallow two files with same file name | 53 | | `fileExtension(ext: string[])` | Required file extensions | 54 | | `fileType(types: string[] \| RegExp)` | Required Mime Types | 55 | | `maxFileCount(count: number)` | Max number of files | 56 | | `maxFileSize(bytes: number)` | Max bytes allowed per file | 57 | | `maxTotalSize(bytes: number)` | Max total input size | 58 | | `required` | At least one file required | 59 | -------------------------------------------------------------------------------- /projects/ngx-file-drag-drop/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-file-drag-drop", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/ngx-file-drag-drop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-file-drag-drop", 3 | "version": "11.0.0", 4 | "peerDependencies": { 5 | "@angular/common": "^19.0.0", 6 | "@angular/core": "^19.0.0", 7 | "@angular/cdk": "^19.0.0", 8 | "@angular/material": "^19.0.0" 9 | }, 10 | "description": "angular material file input component supports file drag and drop, and selection with native file picker", 11 | "homepage": "https://github.com/telebroad/ngx-file-drag-drop", 12 | "keywords": [ 13 | "angular", 14 | "material", 15 | "file", 16 | "input", 17 | "component", 18 | "file-picker", 19 | "drag-and-drop", 20 | "file-drop", 21 | "ngx-file-drag-drop" 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/telebroad/ngx-file-drag-drop/issues" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/telebroad/ngx-file-drag-drop" 29 | }, 30 | "dependencies": { 31 | "tslib": "^2.3.0" 32 | }, 33 | "sideEffects": false 34 | } 35 | -------------------------------------------------------------------------------- /projects/ngx-file-drag-drop/src/lib/byte.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from "@angular/core"; 2 | 3 | @Pipe({ 4 | name: "byte", 5 | standalone: true, 6 | }) 7 | export class BytePipe implements PipeTransform { 8 | transform(value: string | number, decimals: number | string = 2): string { 9 | value = value.toString(); 10 | if (parseInt(value, 10) >= 0) { 11 | value = this.formatBytes(+value, +decimals); 12 | } 13 | return value; 14 | } 15 | 16 | // https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript 17 | formatBytes(bytes: number, decimals = 2): string { 18 | if (bytes === 0) { 19 | return "0 Bytes"; 20 | } 21 | 22 | const k = 1024; 23 | const dm = decimals < 0 ? 0 : decimals; 24 | const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 25 | 26 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 27 | 28 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /projects/ngx-file-drag-drop/src/lib/ngx-file-drag-drop.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | EventEmitter, 5 | HostBinding, 6 | HostListener, 7 | Input, 8 | Output, 9 | ViewChild, 10 | forwardRef, 11 | } from "@angular/core"; 12 | import { BytePipe } from "./byte.pipe"; 13 | import { MatChipsModule } from "@angular/material/chips"; 14 | import { MatTooltipModule } from "@angular/material/tooltip"; 15 | import { coerceBooleanProperty } from "@angular/cdk/coercion"; 16 | import { MatIconModule } from "@angular/material/icon"; 17 | import { NG_VALUE_ACCESSOR } from "@angular/forms"; 18 | 19 | @Component({ 20 | selector: "ngx-file-drag-drop", 21 | standalone: true, 22 | providers: [ 23 | { 24 | provide: NG_VALUE_ACCESSOR, 25 | useExisting: forwardRef(() => NgxFileDragDropComponent), 26 | multi: true, 27 | }, 28 | ], 29 | imports: [MatChipsModule, MatTooltipModule, MatIconModule, BytePipe], 30 | template: ` 31 | @if(files.length){ 32 | 33 | @for (file of files; track file) { 34 | 46 | {{ getFileName(file) }} 47 | @if(!disabled){ 48 | cancel 49 | } 50 | 51 | } 52 | 53 | } @if (!files.length){ 54 | {{ emptyPlaceholder }} 55 | } 56 | 64 | `, 65 | styles: ` 66 | input { 67 | width: 0px; 68 | height: 0px; 69 | opacity: 0; 70 | overflow: hidden; 71 | position: absolute; 72 | z-index: -1; 73 | } 74 | 75 | :host { 76 | display: block; 77 | border: 2px dashed; 78 | border-radius: 20px; 79 | min-height: 50px; 80 | margin: 10px auto; 81 | max-width: 500px; 82 | padding: 20px; 83 | cursor: pointer; 84 | } 85 | :host.disabled { 86 | opacity: 0.5; 87 | cursor: unset; 88 | } 89 | 90 | .placeholder { 91 | color: grey; 92 | white-space: nowrap; 93 | overflow: hidden; 94 | text-overflow: ellipsis; 95 | } 96 | 97 | mat-chip { 98 | max-width: 100%; 99 | } 100 | .filename { 101 | max-width: calc(100% - 1em); 102 | white-space: nowrap; 103 | overflow: hidden; 104 | text-overflow: ellipsis; 105 | } 106 | 107 | :host.empty-input { 108 | display: flex; 109 | align-items: center; 110 | justify-content: center; 111 | } 112 | 113 | .mat-mdc-chip.mat-mdc-standard-chip.mat-focus-indicator { 114 | box-shadow: none; 115 | } 116 | 117 | .mat-mdc-chip.mat-mdc-standard-chip::after { 118 | background: unset; 119 | } 120 | `, 121 | }) 122 | export class NgxFileDragDropComponent { 123 | @HostBinding("class.disabled") 124 | @Input() 125 | get disabled() { 126 | return this._disabled; 127 | } 128 | set disabled(val: boolean) { 129 | this._disabled = coerceBooleanProperty(val); 130 | } 131 | @Input() 132 | set multiple(value: boolean) { 133 | this._multiple = coerceBooleanProperty(value); 134 | } 135 | get multiple() { 136 | return this._multiple; 137 | } 138 | 139 | @Input() 140 | set displayFileSize(value: boolean) { 141 | this._displayFileSize = coerceBooleanProperty(value); 142 | } 143 | get displayFileSize() { 144 | return this._displayFileSize; 145 | } 146 | 147 | @Input() 148 | @HostBinding("style.border-color") 149 | set activeBorderColor(color: string) { 150 | this._activeBorderColor = color; 151 | } 152 | get activeBorderColor() { 153 | return this.isDragover ? this._activeBorderColor : "#ccc"; 154 | } 155 | get files() { 156 | return this._files; 157 | } 158 | 159 | @HostBinding("class.empty-input") 160 | get isEmpty() { 161 | return !this.files?.length; 162 | } 163 | 164 | // @HostBinding('class.drag-over') 165 | get isDragover() { 166 | return this._isDragOver; 167 | } 168 | set isDragover(value: boolean) { 169 | if (!this.disabled) { 170 | this._isDragOver = value; 171 | } 172 | } 173 | 174 | @Output() 175 | private valueChanged = new EventEmitter(); 176 | 177 | @ViewChild("fileInputEl") 178 | private fileInputEl: ElementRef | undefined; 179 | 180 | // does no validation, just sets the hidden file input 181 | @Input() accept = "*"; 182 | 183 | private _disabled = false; 184 | 185 | _multiple = false; 186 | 187 | @Input() emptyPlaceholder = `Drop file${ 188 | this.multiple ? "s" : "" 189 | } or click to select`; 190 | 191 | private _displayFileSize = false; 192 | 193 | private _activeBorderColor = "purple"; 194 | 195 | private _files: File[] = []; 196 | private _isDragOver = false; 197 | 198 | // https://angular.io/api/forms/ControlValueAccessor 199 | private _onChange = (_: File[]) => {}; 200 | private _onTouched = () => {}; 201 | 202 | writeValue(files: File[]): void { 203 | const fileArray = this.convertToArray(files); 204 | if (fileArray.length < 2 || this.multiple) { 205 | this._files = fileArray; 206 | this.emitChanges(this._files); 207 | } else { 208 | throw Error("Multiple files not allowed"); 209 | } 210 | } 211 | registerOnChange(fn: any): void { 212 | this._onChange = fn; 213 | } 214 | registerOnTouched(fn: any): void { 215 | this._onTouched = fn; 216 | } 217 | setDisabledState?(isDisabled: boolean): void { 218 | this.disabled = isDisabled; 219 | } 220 | 221 | private emitChanges(files: File[]) { 222 | this.valueChanged.emit(files); 223 | this._onChange(files); 224 | } 225 | 226 | addFiles(files: File[] | FileList | File) { 227 | // this._onTouched(); 228 | 229 | const fileArray = this.convertToArray(files); 230 | 231 | if (this.multiple) { 232 | // this.errorOnEqualFilenames(fileArray); 233 | const merged = this.files.concat(fileArray); 234 | this.writeValue(merged); 235 | } else { 236 | this.writeValue(fileArray); 237 | } 238 | } 239 | 240 | removeFile(file: File) { 241 | const fileIndex = this.files.indexOf(file); 242 | if (fileIndex >= 0) { 243 | const currentFiles = this.files.slice(); 244 | currentFiles.splice(fileIndex, 1); 245 | this.writeValue(currentFiles); 246 | } 247 | } 248 | 249 | clear() { 250 | this.writeValue([]); 251 | } 252 | 253 | @HostListener("change", ["$event"]) 254 | change(event: Event) { 255 | event.stopPropagation(); 256 | this._onTouched(); 257 | const fileList = (event.target as HTMLInputElement).files; 258 | if (fileList?.length) { 259 | this.addFiles(fileList); 260 | } 261 | // clear it so change is triggered if same file is selected again 262 | (event.target as HTMLInputElement).value = ""; 263 | } 264 | 265 | @HostListener("dragenter", ["$event"]) 266 | @HostListener("dragover", ["$event"]) 267 | activate(e: Event) { 268 | e.preventDefault(); 269 | this.isDragover = true; 270 | } 271 | 272 | @HostListener("dragleave", ["$event"]) 273 | deactivate(e: { 274 | dataTransfer?: { files: FileList }; 275 | preventDefault(): void; 276 | }) { 277 | e.preventDefault(); 278 | this.isDragover = false; 279 | } 280 | 281 | @HostListener("drop", ["$event"]) 282 | handleDrop(e: { dataTransfer: { files: FileList }; preventDefault(): void }) { 283 | this.deactivate(e); 284 | if (!this.disabled) { 285 | const fileList = e.dataTransfer.files; 286 | this.removeDirectories(fileList).then((files: File[]) => { 287 | if (files?.length) { 288 | this.addFiles(files); 289 | } 290 | this._onTouched(); 291 | }); 292 | } 293 | } 294 | 295 | @HostListener("click") 296 | open() { 297 | if (!this.disabled) { 298 | this.fileInputEl?.nativeElement.click(); 299 | } 300 | } 301 | 302 | private removeDirectories(files: FileList): Promise { 303 | return new Promise((resolve) => { 304 | const fileArray = this.convertToArray(files); 305 | 306 | const dirnames: string[] = []; 307 | 308 | const readerList = []; 309 | 310 | for (let i = 0; i < fileArray.length; i++) { 311 | const reader = new FileReader(); 312 | 313 | reader.onerror = () => { 314 | dirnames.push(fileArray[i].name); 315 | }; 316 | 317 | reader.onloadend = () => addToReaderList(i); 318 | 319 | reader.readAsArrayBuffer(fileArray[i]); 320 | } 321 | 322 | function addToReaderList(val: number) { 323 | readerList.push(val); 324 | if (readerList.length === fileArray.length) { 325 | resolve( 326 | fileArray.filter((file: File) => !dirnames.includes(file.name)) 327 | ); 328 | } 329 | } 330 | }); 331 | } 332 | 333 | private convertToArray( 334 | files: FileList | File[] | File | null | undefined 335 | ): File[] { 336 | if (files) { 337 | if (files instanceof File) { 338 | return [files]; 339 | } else if (Array.isArray(files)) { 340 | return files; 341 | } else { 342 | return Array.prototype.slice.call(files); 343 | } 344 | } 345 | return []; 346 | } 347 | 348 | getFileName(file: File): string { 349 | if (!this._displayFileSize) { 350 | return file.name; 351 | } 352 | 353 | const size = new BytePipe().transform(file.size); 354 | return `${file.name} (${size})`; 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /projects/ngx-file-drag-drop/src/lib/validators.ts: -------------------------------------------------------------------------------- 1 | import { ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms"; 2 | 3 | export class FileValidators { 4 | static fileExtension(ext: string[]): ValidatorFn { 5 | return (control: AbstractControl): ValidationErrors | null => { 6 | const validExtensions = ext.map((e) => e.trim().toLowerCase()); 7 | const fileArray = control.value as File[]; 8 | 9 | const invalidFiles = fileArray 10 | .map((file) => file.name) 11 | .filter((fname) => { 12 | const extension = fname 13 | .slice(((fname.lastIndexOf(".") - 1) >>> 0) + 2) 14 | .toLowerCase(); 15 | return !validExtensions.includes(extension); 16 | }) 17 | .map((name) => ({ 18 | name, 19 | ext: name.slice(((name.lastIndexOf(".") - 1) >>> 0) + 2), 20 | })); 21 | 22 | return !invalidFiles.length 23 | ? null 24 | : { 25 | fileExtension: { 26 | requiredExtension: ext.toString(), 27 | actualExtensions: invalidFiles, 28 | }, 29 | }; 30 | }; 31 | } 32 | 33 | static uniqueFileNames(control: AbstractControl): ValidationErrors | null { 34 | const fileNameArray = (control.value as File[]).map((file) => file.name); 35 | const duplicates = fileNameArray.reduce( 36 | (acc: Record, curr) => { 37 | acc[curr] = acc[curr] ? acc[curr] + 1 : 1; 38 | return acc; 39 | }, 40 | {} 41 | ); 42 | 43 | const duplicatesArray: { name: string; count: number }[] = ( 44 | Object.entries(duplicates) as [string, number][] 45 | ) 46 | .filter((arr) => arr[1] > 1) 47 | .map((arr) => ({ name: arr[0], count: arr[1] })); 48 | 49 | return !duplicatesArray.length 50 | ? null 51 | : { 52 | uniqueFileNames: { duplicatedFileNames: duplicatesArray }, 53 | }; 54 | } 55 | 56 | static fileType(types: string[] | RegExp): ValidatorFn { 57 | return (control: AbstractControl): ValidationErrors | null => { 58 | let regExp: RegExp; 59 | if (Array.isArray(types)) { 60 | const joinedTypes = types.join("$|^"); 61 | regExp = new RegExp(`^${joinedTypes}$`, "i"); 62 | } else { 63 | regExp = types; 64 | } 65 | 66 | const fileArray = control.value as File[]; 67 | 68 | const invalidFiles = fileArray 69 | .filter((file) => !regExp.test(file.type)) 70 | .map((file) => ({ name: file.name, type: file.type })); 71 | 72 | return !invalidFiles.length 73 | ? null 74 | : { 75 | fileType: { 76 | requiredType: types.toString(), 77 | actualTypes: invalidFiles, 78 | }, 79 | }; 80 | }; 81 | } 82 | 83 | static maxFileCount(count: number): ValidatorFn { 84 | return (control: AbstractControl): ValidationErrors | null => { 85 | const fileCount = control?.value ? (control.value as File[]).length : 0; 86 | const result = count >= fileCount; 87 | return result 88 | ? null 89 | : { 90 | maxFileCount: { 91 | maxCount: count, 92 | actualCount: fileCount, 93 | }, 94 | }; 95 | }; 96 | } 97 | 98 | static maxFileSize(bytes: number): ValidatorFn { 99 | return (control: AbstractControl): ValidationErrors | null => { 100 | const fileArray = control.value as File[]; 101 | 102 | const invalidFiles = fileArray 103 | .filter((file) => file.size > bytes) 104 | .map((file) => ({ name: file.name, size: file.size })); 105 | 106 | return !invalidFiles.length 107 | ? null 108 | : { 109 | maxFileSize: { 110 | maxSize: bytes, 111 | actualSizes: invalidFiles, 112 | }, 113 | }; 114 | }; 115 | } 116 | 117 | static maxTotalSize(bytes: number): ValidatorFn { 118 | return (control: AbstractControl): ValidationErrors | null => { 119 | const size = control?.value 120 | ? (control.value as File[]) 121 | .map((file) => file.size) 122 | .reduce((acc, i) => acc + i, 0) 123 | : 0; 124 | const result = bytes >= size; 125 | return result 126 | ? null 127 | : { 128 | maxTotalSize: { 129 | maxSize: bytes, 130 | actualSize: size, 131 | }, 132 | }; 133 | }; 134 | } 135 | static required(control: AbstractControl): ValidationErrors | null { 136 | const count = control?.value?.length; 137 | return count 138 | ? null 139 | : { 140 | required: true, 141 | }; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /projects/ngx-file-drag-drop/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-file-drag-drop 3 | */ 4 | 5 | export * from './lib/ngx-file-drag-drop.component'; 6 | export * from './lib/byte.pipe'; 7 | export * from './lib/validators'; 8 | -------------------------------------------------------------------------------- /projects/ngx-file-drag-drop/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-file-drag-drop/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "skipLibCheck": true, 12 | "paths": { 13 | "ngx-file-drag-drop": [ 14 | "./dist/ngx-file-drag-drop" 15 | ] 16 | }, 17 | "esModuleInterop": true, 18 | "sourceMap": true, 19 | "declaration": false, 20 | "experimentalDecorators": true, 21 | "moduleResolution": "bundler", 22 | "importHelpers": true, 23 | "target": "ES2022", 24 | "module": "ES2022", 25 | "useDefineForClassFields": false, 26 | "lib": [ 27 | "ES2022", 28 | "dom" 29 | ] 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef-whitespace": { 86 | "options": [ 87 | { 88 | "call-signature": "nospace", 89 | "index-signature": "nospace", 90 | "parameter": "nospace", 91 | "property-declaration": "nospace", 92 | "variable-declaration": "nospace" 93 | }, 94 | { 95 | "call-signature": "onespace", 96 | "index-signature": "onespace", 97 | "parameter": "onespace", 98 | "property-declaration": "onespace", 99 | "variable-declaration": "onespace" 100 | } 101 | ] 102 | }, 103 | "variable-name": { 104 | "options": [ 105 | "ban-keywords", 106 | "check-format", 107 | "allow-pascal-case" 108 | ] 109 | }, 110 | "whitespace": { 111 | "options": [ 112 | "check-branch", 113 | "check-decl", 114 | "check-operator", 115 | "check-separator", 116 | "check-type", 117 | "check-typecast" 118 | ] 119 | }, 120 | "component-class-suffix": true, 121 | "contextual-lifecycle": true, 122 | "directive-class-suffix": true, 123 | "no-conflicting-lifecycle": true, 124 | "no-host-metadata-property": true, 125 | "no-input-rename": true, 126 | "no-inputs-metadata-property": true, 127 | "no-output-native": true, 128 | "no-output-on-prefix": true, 129 | "no-output-rename": true, 130 | "no-outputs-metadata-property": true, 131 | "template-banana-in-box": true, 132 | "template-no-negated-async": true, 133 | "use-lifecycle-interface": true, 134 | "use-pipe-transform-interface": true 135 | } 136 | } 137 | --------------------------------------------------------------------------------