├── projects
├── demo
│ ├── public
│ │ └── favicon.ico
│ ├── src
│ │ ├── styles.css
│ │ ├── main.ts
│ │ ├── app
│ │ │ ├── app.config.ts
│ │ │ └── app.component.ts
│ │ └── index.html
│ └── tsconfig.app.json
└── ngx-file-drag-drop
│ ├── ng-package.json
│ ├── src
│ ├── public-api.ts
│ └── lib
│ │ ├── byte.pipe.ts
│ │ ├── validators.ts
│ │ └── ngx-file-drag-drop.component.ts
│ ├── tsconfig.lib.prod.json
│ ├── tsconfig.lib.json
│ ├── package.json
│ └── README.md
├── .gitignore
├── package.json
├── tsconfig.json
├── LICENSE
├── README.md
├── tslint.json
└── angular.json
/projects/demo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/telebroad/ngx-file-drag-drop/HEAD/projects/demo/public/favicon.ico
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------