├── src ├── assets │ └── .gitkeep ├── app │ ├── app.component.scss │ ├── demo-file-picker │ │ ├── demo-file-picker.component.scss │ │ ├── demo-file-picker.component.html │ │ ├── demo-file-picker.adapter.ts │ │ └── demo-file-picker.component.ts │ ├── app.component.html │ ├── app.component.ts │ ├── app-routing.module.ts │ └── app.module.ts ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── styles.scss ├── tslint.json ├── main.ts ├── test.ts ├── index.html ├── karma.conf.js └── polyfills.ts ├── angular-image.gif ├── projects └── file-picker │ ├── src │ ├── lib │ │ ├── icons │ │ │ ├── close-icon │ │ │ │ ├── close-icon.component.scss │ │ │ │ ├── close-icon.component.html │ │ │ │ └── close-icon.component.ts │ │ │ └── cloud-icon │ │ │ │ ├── cloud-icon.component.scss │ │ │ │ ├── cloud-icon.component.html │ │ │ │ ├── cloud-icon.component.ts │ │ │ │ └── cloud-icon.component.spec.ts │ │ ├── file-preview-container │ │ │ ├── file-preview-item │ │ │ │ ├── refresh-icon │ │ │ │ │ ├── refresh-icon.component.scss │ │ │ │ │ ├── refresh-icon.component.html │ │ │ │ │ └── refresh-icon.component.ts │ │ │ │ ├── file-preview-item.component.html │ │ │ │ ├── file-preview-item.component.ts │ │ │ │ ├── file-preview-item.component.scss │ │ │ │ └── file-preview-item.component.spec.ts │ │ │ ├── file-preview-container.component.scss │ │ │ ├── preview-lightbox │ │ │ │ ├── preview-lightbox.component.html │ │ │ │ ├── preview-lightbox.component.ts │ │ │ │ └── preview-lightbox.component.scss │ │ │ ├── file-preview-container.component.html │ │ │ └── file-preview-container.component.ts │ │ ├── file-drop │ │ │ ├── upload-event.model.ts │ │ │ ├── index.ts │ │ │ ├── file-drop.module.ts │ │ │ ├── upload-file.model.ts │ │ │ ├── file-drop.component.html │ │ │ ├── file-drop.component.scss │ │ │ ├── dom.types.ts │ │ │ └── file-drop.component.ts │ │ ├── file-preview.model.ts │ │ ├── uploader-captions.ts │ │ ├── file-picker.constants.ts │ │ ├── validation-error.model.ts │ │ ├── default-captions.ts │ │ ├── services │ │ │ └── file-validator │ │ │ │ ├── file-validator.service.spec.ts │ │ │ │ └── file-validator.service.ts │ │ ├── file-picker.adapter.ts │ │ ├── file-upload.utils.ts │ │ ├── file-picker.service.ts │ │ ├── test-utils.ts │ │ ├── file-picker.module.ts │ │ ├── mock-file-picker.adapter.ts │ │ ├── file-picker.component.html │ │ ├── file-picker.component.scss │ │ ├── file-picker.spec.ts │ │ └── file-picker.component.ts │ ├── public_api.ts │ └── test.ts │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── package.json │ ├── karma.conf.js │ ├── tslint.json │ └── README.md ├── .editorconfig ├── tsconfig.app.json ├── .browserslistrc ├── breaking-changes-v10.md ├── tsconfig.spec.json ├── .gitignore ├── tsconfig.json ├── package.json ├── tslint.json ├── angular.json └── README.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/demo-file-picker/demo-file-picker.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vugar005/ngx-awesome-uploader/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /angular-image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vugar005/ngx-awesome-uploader/HEAD/angular-image.gif -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | * { 3 | box-sizing: border-box; 4 | } 5 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/icons/close-icon/close-icon.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | cursor: pointer; 4 | } 5 | 6 | svg { 7 | fill: #95a5a6; 8 | } -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/file-preview-item/refresh-icon/refresh-icon.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | cursor: pointer; 4 | } 5 | 6 | svg { 7 | fill: #95a5a6; 8 | } -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-drop/upload-event.model.ts: -------------------------------------------------------------------------------- 1 | import { UploadFile } from './upload-file.model'; 2 | 3 | export class UploadEvent { 4 | constructor( 5 | public files: UploadFile[]) { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/file-preview-container.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display:flex; 3 | flex-direction: column; 4 | justify-content: flex-start; 5 | width: 100%; 6 | background: #fafbfd; 7 | } 8 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/icons/cloud-icon/cloud-icon.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | margin-bottom: 0.4em; 6 | } 7 | .svg-icon { 8 | fill: #95a5a6; 9 | } -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-drop/index.ts: -------------------------------------------------------------------------------- 1 | export * from './file-drop.component'; 2 | export * from './file-drop.module'; 3 | export * from './upload-file.model'; 4 | export * from './upload-event.model'; 5 | export * from './dom.types'; 6 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview.model.ts: -------------------------------------------------------------------------------- 1 | export interface FilePreviewModel { 2 | /** uploadResponse is the response of api after file uploaded */ 3 | uploadResponse?: any; 4 | file: File | Blob; 5 | fileName: string; 6 | } 7 | -------------------------------------------------------------------------------- /projects/file-picker/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/file-picker", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | }, 7 | "allowedNonPeerDependencies": ["mrmime"] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/icons/close-icon/close-icon.component.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'], 7 | standalone: false 8 | }) 9 | export class AppComponent { 10 | title = 'ngx-awesome-uploader'; 11 | } 12 | -------------------------------------------------------------------------------- /projects/file-picker/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 | "strict": false 7 | }, 8 | "angularCompilerOptions": { 9 | "compilationMode": "partial" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | const routes: Routes = []; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })], 8 | exports: [RouterModule] 9 | }) 10 | export class AppRoutingModule { } 11 | -------------------------------------------------------------------------------- /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 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/uploader-captions.ts: -------------------------------------------------------------------------------- 1 | export interface UploaderCaptions { 2 | dropzone: { 3 | title: string; 4 | or: string; 5 | browse: string; 6 | }; 7 | cropper: { 8 | crop: string; 9 | cancel: string; 10 | }; 11 | previewCard: { 12 | remove: string; 13 | uploadError: string; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-picker.constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_CROPPER_OPTIONS = { 2 | dragMode: 'crop', 3 | aspectRatio: 1, 4 | autoCrop: true, 5 | movable: true, 6 | zoomable: true, 7 | scalable: true, 8 | autoCropArea: 0.8 9 | }; 10 | 11 | export function bitsToMB(size: number): number { 12 | return parseFloat(size.toString()) / 1048576; 13 | } 14 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/file-picker/tsconfig.spec.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/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/file-picker/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of file-picker 3 | */ 4 | 5 | export * from './lib/file-picker.service'; 6 | export * from './lib/file-picker.component'; 7 | export * from './lib/file-picker.module'; 8 | export * from './lib/file-picker.adapter'; 9 | export * from './lib/file-preview.model'; 10 | export * from './lib/validation-error.model'; 11 | export * from './lib/uploader-captions'; 12 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | 14 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /projects/file-picker/src/lib/validation-error.model.ts: -------------------------------------------------------------------------------- 1 | export interface ValidationError { 2 | file: File; 3 | error: string; // is FileValidationType enum type 4 | } 5 | export enum FileValidationTypes { 6 | fileMaxSize = 'FILE_MAX_SIZE', 7 | fileMaxCount = 'FILE_MAX_COUNT', 8 | totalMaxSize = 'TOTAL_MAX_SIZE', 9 | extensions = 'EXTENSIONS', 10 | uploadType = 'UPLOAD_TYPE', 11 | customValidator = 'CUSTOM_VALIDATOR' 12 | } 13 | -------------------------------------------------------------------------------- /breaking-changes-v10.md: -------------------------------------------------------------------------------- 1 | # Breaking changes from V10. 2 | 3 | 1. ```ngx-file-picker ``` renamed to ``` ngx-awesome-uploader ``` 4 | 2. ```FilePickerAdapter ``` implementation changed. Now you can set BE upload response to body field and use it in item template or removeFile api. 5 | 3. ``` FilePreviewModel ``` interface has changed. ```fileId``` field removed and replaced with ```uploadResponse``` which is basically upload response from BE after file uploaded. 6 | -------------------------------------------------------------------------------- /projects/file-picker/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 | "strict": false 11 | }, 12 | "exclude": [ 13 | "src/test.ts", 14 | "**/*.spec.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/file-preview-item/refresh-icon/refresh-icon.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/file-preview-item/refresh-icon/refresh-icon.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'refresh-icon', 5 | templateUrl: './refresh-icon.component.html', 6 | styleUrls: ['./refresh-icon.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | standalone: false 9 | }) 10 | export class RefreshIconComponent { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/default-captions.ts: -------------------------------------------------------------------------------- 1 | import { UploaderCaptions } from './uploader-captions'; 2 | 3 | export const DefaultCaptions: UploaderCaptions = { 4 | dropzone: { 5 | title: 'Drag and drop file here', 6 | or: 'or', 7 | browse: 'Browse Files' 8 | }, 9 | cropper: { 10 | crop: 'Crop', 11 | cancel: 'Cancel' 12 | }, 13 | previewCard: { 14 | remove: 'Remove', 15 | uploadError: 'Error on upload' 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/preview-lightbox/preview-lightbox.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | 8 |
9 | 12 |
13 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/icons/cloud-icon/cloud-icon.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/icons/close-icon/close-icon.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'close-icon', 5 | templateUrl: './close-icon.component.html', 6 | styleUrls: ['./close-icon.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | standalone: false 9 | }) 10 | export class CloseIconComponent implements OnInit { 11 | 12 | constructor() { } 13 | 14 | ngOnInit() { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/icons/cloud-icon/cloud-icon.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'cloud-icon', 5 | templateUrl: './cloud-icon.component.html', 6 | styleUrls: ['./cloud-icon.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | standalone: false 9 | }) 10 | export class CloudIconComponent implements OnInit { 11 | 12 | constructor() { } 13 | 14 | ngOnInit() { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/services/file-validator/file-validator.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { FileValidatorService } from './file-validator.service'; 4 | 5 | describe('FileValidatorService', () => { 6 | let service: FileValidatorService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(FileValidatorService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-drop/file-drop.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import {FileComponent} from './file-drop.component'; 4 | import { CloudIconComponent } from '../icons/cloud-icon/cloud-icon.component'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | FileComponent, 9 | CloudIconComponent 10 | ], 11 | exports: [FileComponent], 12 | imports: [CommonModule], 13 | providers: [], 14 | bootstrap: [FileComponent], 15 | }) 16 | export class FileDropModule {} 17 | -------------------------------------------------------------------------------- /projects/file-picker/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | // First, initialize the Angular testing environment. 12 | getTestBed().initTestEnvironment( 13 | BrowserDynamicTestingModule, 14 | platformBrowserDynamicTesting(), 15 | ); 16 | -------------------------------------------------------------------------------- /tsconfig.spec.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/spec", 6 | "types": [ 7 | "jasmine" 8 | ], 9 | "paths": { 10 | "core-js/es7/reflect": [ 11 | "node_modules/core-js/proposals/reflect-metadata", 12 | ], 13 | } 14 | }, 15 | "files": [ 16 | "src/test.ts", 17 | "src/polyfills.ts" 18 | ], 19 | "include": [ 20 | "src/**/*.spec.ts", 21 | "src/**/*.d.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), { 14 | teardown: { destroyAfterEach: false } 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgxAwesomeUploader 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-drop/upload-file.model.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemEntry, FileSystemFileEntry, FileSystemDirectoryEntry } from './dom.types'; 2 | 3 | /** 4 | * fileEntry is an instance of {@link FileSystemFileEntry} or {@link FileSystemDirectoryEntry}. 5 | * Which one is it can be checked using {@link FileSystemEntry.isFile} or {@link FileSystemEntry.isDirectory} 6 | * properties of the given {@link FileSystemEntry}. 7 | */ 8 | export class UploadFile { 9 | constructor( 10 | public relativePath: string, 11 | public fileEntry: FileSystemEntry) { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-picker.adapter.ts: -------------------------------------------------------------------------------- 1 | import { FilePreviewModel } from './file-preview.model'; 2 | import { Observable } from 'rxjs'; 3 | 4 | export interface UploadResponse { 5 | body?: any; 6 | status: UploadStatus; 7 | progress?: number; 8 | } 9 | 10 | export enum UploadStatus { 11 | UPLOADED = 'UPLOADED', 12 | IN_PROGRESS = 'IN PROGRESS', 13 | ERROR = 'ERROR' 14 | } 15 | 16 | export abstract class FilePickerAdapter { 17 | public abstract uploadFile(fileItem: FilePreviewModel): Observable; 18 | public abstract removeFile(fileItem: FilePreviewModel): Observable; 19 | } 20 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/file-preview-container.component.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-upload.utils.ts: -------------------------------------------------------------------------------- 1 | export function GET_FILE_CATEGORY_TYPE(fileExtension: string): string { 2 | if (fileExtension.includes('image')) { 3 | return 'image'; 4 | } else if (fileExtension.includes('video')) { 5 | return 'video'; 6 | } else { 7 | return 'other'; 8 | } 9 | } 10 | 11 | export function GET_FILE_TYPE(name: string): string { 12 | return name.split('.').pop().toUpperCase(); 13 | } 14 | 15 | export function IS_IMAGE_FILE(fileType: string): boolean { 16 | const IMAGE_TYPES = ['PNG', 'JPG', 'JPEG', 'BMP', 'WEBP', 'JFIF', 'TIFF']; 17 | return (IMAGE_TYPES as any).includes(fileType.toUpperCase()); 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.angular/cache 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { DemoFilePickerComponent } from './demo-file-picker/demo-file-picker.component'; 6 | import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; 7 | import { FilePickerModule } from 'projects/file-picker/src/public_api'; 8 | 9 | @NgModule({ declarations: [ 10 | AppComponent, 11 | DemoFilePickerComponent 12 | ], 13 | bootstrap: [AppComponent], imports: [BrowserModule, 14 | FilePickerModule], providers: [provideHttpClient(withInterceptorsFromDi())] }) 15 | export class AppModule { } 16 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/icons/cloud-icon/cloud-icon.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { CloudIconComponent } from './cloud-icon.component'; 5 | 6 | describe('CloudIconComponent', () => { 7 | let component: CloudIconComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach((() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ CloudIconComponent ] 13 | }) 14 | .compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(CloudIconComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-drop/file-drop.component.html: -------------------------------------------------------------------------------- 1 |
4 | 5 |
6 | 7 |
8 | 9 |
10 | 11 |
12 | {{captions?.dropzone?.title}} 13 |
14 |
15 | {{captions?.dropzone?.or}} 16 |
17 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-picker.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { of, Observable } from 'rxjs'; 3 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; 4 | 5 | @Injectable() 6 | export class FilePickerService { 7 | constructor(private sanitizer: DomSanitizer) { } 8 | mockUploadFile(formData): Observable { 9 | const event = new CustomEvent('customevent', { 10 | detail: { 11 | type: 'UploadProgreess' 12 | } 13 | }); 14 | return of (event.detail); 15 | } 16 | 17 | // @ts-ignore: Not all code paths return a value 18 | createSafeUrl(file): SafeResourceUrl { 19 | try { 20 | const url = window.URL.createObjectURL(file); 21 | const safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url); 22 | return safeUrl; 23 | } catch (er) { 24 | console.log(er); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { FilePreviewModel } from './file-preview.model'; 2 | import { FilePickerAdapter } from './file-picker.adapter'; 3 | import { of, Observable } from 'rxjs'; 4 | import { delay } from 'rxjs/operators'; 5 | 6 | export function createMockFile(name: string, type: string, sizeInMb = 1): File { 7 | const file = new File([''], name, { type }); 8 | Object.defineProperty(file, 'size', { value: 1048576 * sizeInMb }); 9 | return file; 10 | } 11 | 12 | export function createMockPreviewFile(name: string, type: string, sizeInMb = 1): FilePreviewModel { 13 | const file = createMockFile(name, type, sizeInMb); 14 | return {file, fileName: name}; 15 | } 16 | 17 | export function mockCustomValidator(file: File): Observable { 18 | console.log(file.name.length); 19 | if (!file.name.includes('uploader')) { 20 | return of(true).pipe(delay(2000)); 21 | } 22 | return of(false).pipe(delay(2000)); 23 | } 24 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-drop/file-drop.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | width: 100%; 4 | padding: 0 16px; 5 | } 6 | #dropZone { 7 | max-width: 440px; 8 | margin: auto; 9 | border: 2px dashed #ecf0f1; 10 | // border: 2px dashed #c2cdda; 11 | border-radius: 6px; 12 | padding: 56px 0; 13 | background: #ffffff; 14 | } 15 | .file-browse-button { 16 | padding: 12px 18px; 17 | background: #7f8c8d; 18 | // background: linear-gradient(135deg, #3a8ffe 0, #9658fe 100%); 19 | border: 0; 20 | outline: 0; 21 | font-size: 14px; 22 | color: #ffffff; 23 | font-weight: 700; 24 | border-radius: 6px; 25 | cursor: pointer; 26 | } 27 | .content { 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: center; 31 | align-items: center; 32 | } 33 | .over { 34 | background-color: rgba(147, 147, 147, 0.5); 35 | } 36 | .content-top-text { 37 | font-size: 18px; 38 | font-weight: bold; 39 | font-weight: bold; 40 | color: #5b5b7b; 41 | } 42 | .content-center-text { 43 | color: #90a0bc; 44 | margin: 12px 0; 45 | font-size: 14px; 46 | } 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/preview-lightbox/preview-lightbox.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; 2 | import { FilePreviewModel } from '../../file-preview.model'; 3 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; 4 | 5 | @Component({ 6 | selector: 'preview-lightbox', 7 | templateUrl: './preview-lightbox.component.html', 8 | styleUrls: ['./preview-lightbox.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | standalone: false 11 | }) 12 | export class PreviewLightboxComponent implements OnInit { 13 | @Input() file: FilePreviewModel; 14 | @Output() previewClose = new EventEmitter(); 15 | loaded: boolean; 16 | safeUrl: SafeResourceUrl; 17 | constructor(private sanitizer: DomSanitizer) { } 18 | 19 | ngOnInit() { 20 | this.getSafeUrl(this.file.file); 21 | } 22 | public getSafeUrl(file: File | Blob): void { 23 | const url = window.URL.createObjectURL(file); 24 | this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url); 25 | } 26 | public onClose(event): void { 27 | this.previewClose.next(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /projects/file-picker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-awesome-uploader", 3 | "version": "19.0.1", 4 | "description": "Angular Library for uploading files with Real-Time Progress bar, File Preview, Drag && Drop and Custom Template with Multi Language support", 5 | "peerDependencies": { 6 | "@angular/common": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0 || ^14.0 || ^15.0 || ^16.0 || ^17.0 || ^18.0", 7 | "@angular/core": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0 || ^14.0 || ^15.0 || ^16.0 || ^17.0 || ^18.0" 8 | }, 9 | "keywords": [ 10 | "ngx", 11 | "angular", 12 | "file uploader", 13 | "angular file uploader", 14 | "awesome uploader", 15 | "drag and drop", 16 | "image cropper", 17 | "angular image uploader", 18 | "angular uploader", 19 | "angular file upload" 20 | ], 21 | "license": "MIT", 22 | "repository": "https://github.com/vugar005/ngx-awesome-uploader", 23 | "author": { 24 | "name" : "Vugar Abdullayev", 25 | "url": "https://twitter.com/vugar005", 26 | "email": "https://twitter.com/vugar005" 27 | }, 28 | "dependencies": { 29 | "tslib": "^2.0.0", 30 | "mrmime": "1.0.0" 31 | }, 32 | "homepage": "https://ngx-awesome-uploader.stackblitz.io/" 33 | 34 | } 35 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-picker.module.ts: -------------------------------------------------------------------------------- 1 | import { CloseIconComponent } from './icons/close-icon/close-icon.component'; 2 | import { FilePreviewItemComponent } from './file-preview-container/file-preview-item/file-preview-item.component'; 3 | import { FilePreviewContainerComponent } from './file-preview-container/file-preview-container.component'; 4 | import { NgModule } from '@angular/core'; 5 | import { FilePickerComponent } from './file-picker.component'; 6 | import { CommonModule } from '@angular/common'; 7 | import { FilePickerService } from './file-picker.service'; 8 | import { FileDropModule } from './file-drop/file-drop.module'; 9 | import { PreviewLightboxComponent } from './file-preview-container/preview-lightbox/preview-lightbox.component'; 10 | import { RefreshIconComponent } from './file-preview-container/file-preview-item/refresh-icon/refresh-icon.component'; 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | FileDropModule, 15 | ], 16 | declarations: [ 17 | FilePickerComponent, 18 | FilePreviewContainerComponent, 19 | FilePreviewItemComponent, 20 | PreviewLightboxComponent, 21 | RefreshIconComponent, 22 | CloseIconComponent 23 | ], 24 | exports: [FilePickerComponent], 25 | providers: [FilePickerService] 26 | }) 27 | export class FilePickerModule {} 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "esModuleInterop": true, 9 | "strict": false, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "sourceMap": true, 15 | "paths": { 16 | "ngx-awesome-uploader": [ 17 | "projects/file-picker/src/public_api" 18 | ], 19 | "file-picker": [ 20 | "dist/file-picker" 21 | ], 22 | "file-picker/*": [ 23 | "dist/file-picker/*" 24 | ] 25 | }, 26 | "declaration": false, 27 | "experimentalDecorators": true, 28 | "moduleResolution": "node", 29 | "importHelpers": true, 30 | "target": "ES2022", 31 | "module": "es2020", 32 | "lib": [ 33 | "es2020", 34 | "dom" 35 | ], 36 | "useDefineForClassFields": false 37 | }, 38 | "angularCompilerOptions": { 39 | "enableI18nLegacyMessageIdFormat": false, 40 | "strictInjectionParameters": true, 41 | "strictInputAccessModifiers": true, 42 | "strictTemplates": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-awesome-uploader-repo", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "build:lib": "ng build file-picker", 9 | "test:lib": "ng test file-picker", 10 | "test": "ng test" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "19.1.7", 15 | "@angular/common": "19.1.7", 16 | "@angular/compiler": "19.1.7", 17 | "@angular/core": "19.1.7", 18 | "@angular/forms": "19.1.7", 19 | "@angular/platform-browser": "19.1.7", 20 | "@angular/platform-browser-dynamic": "19.1.7", 21 | "@angular/router": "19.1.7", 22 | "mrmime": "1.0.0", 23 | "rxjs": "7.8.0", 24 | "tslib": "^2.3.0", 25 | "zone.js": "~0.15.0" 26 | }, 27 | "devDependencies": { 28 | "@angular-devkit/build-angular": "19.1.8", 29 | "@angular/cli": "19.1.8", 30 | "@angular/compiler-cli": "19.1.7", 31 | "@types/jasmine": "~3.10.0", 32 | "@types/node": "^12.11.1", 33 | "jasmine-core": "~3.10.0", 34 | "karma": "~6.3.0", 35 | "karma-chrome-launcher": "~3.1.0", 36 | "karma-coverage": "~2.0.3", 37 | "karma-jasmine": "~4.0.0", 38 | "karma-jasmine-html-reporter": "~1.7.0", 39 | "ng-packagr": "19.2.0", 40 | "typescript": "~5.5.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-drop/dom.types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface FileSystemEntry { 3 | name: string; 4 | isDirectory: boolean; 5 | isFile: boolean; 6 | } 7 | 8 | export interface FileSystemEntryMetadata { 9 | modificationTime?: Date; 10 | size?: number; 11 | } 12 | 13 | export interface FileSystemDirectoryReader { 14 | readEntries( 15 | successCallback: (result: FileSystemEntry[]) => void, 16 | errorCallback?: (error: any) => void, 17 | ): void; 18 | } 19 | 20 | export interface FileSystemFlags { 21 | create?: boolean; 22 | exclusive?: boolean; 23 | } 24 | 25 | export interface FileSystemDirectoryEntry extends FileSystemEntry { 26 | isDirectory: true; 27 | isFile: false; 28 | createReader(): FileSystemDirectoryReader; 29 | getFile( 30 | path?: string, 31 | options?: FileSystemFlags, 32 | successCallback?: (result: FileSystemFileEntry) => void, 33 | errorCallback?: (error: any) => void, 34 | ): void; 35 | getDirectory( 36 | path?: string, 37 | options?: FileSystemFlags, 38 | successCallback?: (result: FileSystemDirectoryEntry) => void, 39 | errorCallback?: (error: any) => void, 40 | ): void; 41 | } 42 | 43 | export interface FileSystemFileEntry extends FileSystemEntry { 44 | isDirectory: false; 45 | isFile: true; 46 | file(callback: (file: File) => void): void; 47 | } 48 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/preview-lightbox/preview-lightbox.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | position: fixed; /* Stay in place */ 7 | z-index: 1040; /* Sit on top */ 8 | left: 0; 9 | top: 0; 10 | width: 100vw; /* Full width */ 11 | height: 100vh; /* Full height */ 12 | overflow: auto; /* Enable scroll if needed */ 13 | overflow: hidden; 14 | } 15 | .ng-modal-backdrop { 16 | position: fixed; 17 | top: 0; 18 | right: 0; 19 | bottom: 0; 20 | left: 0; 21 | z-index: 1040; 22 | background: rgba(0,0,0,.288); 23 | } 24 | .ng-modal-content { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | color: rgba(0,0,0,.87); 29 | z-index: 1041; 30 | .close-icon-wrapper { 31 | position: absolute; 32 | top: 20px; 33 | right: 20px; 34 | font-size: 20px; 35 | } 36 | .lightbox-item { 37 | img { 38 | max-width: calc(100vw - 30px); 39 | max-height: calc(100vh - 30px); 40 | width: 100%; 41 | height: auto; 42 | object-fit: contain; 43 | animation-name: zoomIn; 44 | animation-duration: 200ms; 45 | } 46 | } 47 | } 48 | @keyframes zoomIn { 49 | from { 50 | opacity: 0; 51 | transform: scale3d(0.9, 0.9, 0.9); 52 | } 53 | 54 | 50% { 55 | opacity: 1; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/demo-file-picker/demo-file-picker.component.html: -------------------------------------------------------------------------------- 1 |
2 | 21 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |

{{fileItem.file?.size}}

31 |

{{fileItem.fileName}}

32 |

{{uploadProgress}}

33 | 34 |
35 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/file-preview-container.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter, TemplateRef, ChangeDetectionStrategy } from '@angular/core'; 2 | import { FilePreviewModel } from '../file-preview.model'; 3 | import { FilePickerAdapter } from '../file-picker.adapter'; 4 | import { UploaderCaptions } from '../uploader-captions'; 5 | import { HttpErrorResponse } from '@angular/common/http'; 6 | 7 | @Component({ 8 | selector: 'file-preview-container', 9 | templateUrl: './file-preview-container.component.html', 10 | styleUrls: ['./file-preview-container.component.scss'], 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | standalone: false 13 | }) 14 | export class FilePreviewContainerComponent implements OnInit { 15 | @Input() previewFiles: FilePreviewModel[]; 16 | @Input() itemTemplate: TemplateRef; 17 | @Input() enableAutoUpload: boolean; 18 | @Output() public readonly removeFile = new EventEmitter(); 19 | @Output() public readonly uploadSuccess = new EventEmitter(); 20 | @Output() public readonly uploadFail = new EventEmitter(); 21 | public lightboxFile: FilePreviewModel; 22 | @Input() adapter: FilePickerAdapter; 23 | @Input() captions: UploaderCaptions; 24 | constructor( 25 | ) { } 26 | 27 | ngOnInit() { 28 | } 29 | 30 | public openLightbox(file: FilePreviewModel): void { 31 | this.lightboxFile = file; 32 | } 33 | 34 | public closeLightbox(): void { 35 | this.lightboxFile = undefined; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/angularV13'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /projects/file-picker/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/file-picker'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/mock-file-picker.adapter.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HttpRequest, HttpClient, HttpEvent, HttpEventType } from '@angular/common/http'; 3 | import { catchError, map } from 'rxjs/operators'; 4 | import { Observable, of } from 'rxjs'; 5 | import { FilePickerAdapter, UploadResponse, UploadStatus, FilePreviewModel } from 'ngx-awesome-uploader'; 6 | 7 | export class DemoFilePickerAdapter extends FilePickerAdapter { 8 | constructor(private http: HttpClient) { 9 | super(); 10 | } 11 | public uploadFile(fileItem: FilePreviewModel): Observable { 12 | const form = new FormData(); 13 | form.append('file', fileItem.file); 14 | const api = 'https://ngx-awesome-uploader.free.beeceptor.com/upload'; 15 | const req = new HttpRequest('POST', api, form, {reportProgress: true}); 16 | return this.http.request(req) 17 | .pipe( 18 | map((res: HttpEvent) => { 19 | if (res.type === HttpEventType.Response) { 20 | const responseFromBackend = res.body; 21 | return { 22 | body: responseFromBackend, 23 | status: UploadStatus.UPLOADED 24 | }; 25 | } else if (res.type === HttpEventType.UploadProgress) { 26 | /** Compute and show the % done: */ 27 | const uploadProgress = +Math.round((100 * res.loaded) / res.total); 28 | return { 29 | status: UploadStatus.IN_PROGRESS, 30 | progress: uploadProgress 31 | }; 32 | } 33 | }), 34 | catchError(er => { 35 | console.log(er); 36 | return of({status: UploadStatus.ERROR, body: er }); 37 | }) 38 | ); 39 | } 40 | public removeFile(fileItem: FilePreviewModel): Observable { 41 | const id = 50; 42 | const responseFromBackend = fileItem.uploadResponse; 43 | console.log(fileItem); 44 | const removeApi = 'https://run.mocky.io/v3/dedf88ec-7ce8-429a-829b-bd2fc55352bc'; 45 | return this.http.post(removeApi, {id}); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-picker.component.html: -------------------------------------------------------------------------------- 1 |
6 | 11 | 12 | 13 |
14 | 15 | 27 | 28 |
29 |
30 | 35 |
36 | 43 | 55 |
56 |
57 |
58 | 59 |
63 | 74 | 75 |
76 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/services/file-validator/file-validator.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { bitsToMB } from '../../file-picker.constants'; 3 | import { FilePreviewModel } from '../../file-preview.model'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class FileValidatorService { 9 | 10 | constructor() { } 11 | 12 | /** Validates file extension */ 13 | public isValidExtension(fileName: string, fileExtensions: string[]): boolean { 14 | if (!fileExtensions?.length) { return true; } 15 | 16 | const extension: string = fileName.split('.').pop(); 17 | const fileExtensionsLowercase = fileExtensions.map(ext => ext.toLowerCase()); 18 | if (fileExtensionsLowercase.indexOf(extension.toLowerCase()) === -1) { 19 | return false; 20 | } 21 | return true; 22 | } 23 | 24 | /** Validates if upload type is single so another file cannot be added */ 25 | public isValidUploadType(files: FilePreviewModel[], uploadType: string): boolean { 26 | if (uploadType === 'single' && files?.length > 0) { 27 | return false; 28 | } else { 29 | return true; 30 | } 31 | } 32 | 33 | /** Validates max file count */ 34 | public isValidMaxFileCount(fileMaxCount: number, newFiles: File[], files: FilePreviewModel[]): boolean { 35 | if (!fileMaxCount || fileMaxCount >= files?.length + newFiles?.length) { 36 | return true; 37 | } else { 38 | return false; 39 | } 40 | } 41 | 42 | public isValidFileSize(size: number, fileMaxSize: number) { 43 | const fileMB: number = bitsToMB(size); 44 | if (!fileMaxSize || (fileMaxSize && fileMB < fileMaxSize)) { 45 | return true; 46 | } else { 47 | return false; 48 | } 49 | } 50 | 51 | public isValidTotalFileSize(newFile: File, files: FilePreviewModel[], totalMaxSize: number) { 52 | /** Validating Total Files Size */ 53 | const totalBits = files 54 | .map(f => f.file ? f.file.size : 0) 55 | .reduce((acc, curr) => acc + curr, 0); 56 | 57 | if (!totalMaxSize || (totalMaxSize && bitsToMB(totalBits + newFile.size) < totalMaxSize)) { 58 | return true; 59 | } else { 60 | return false; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/app/demo-file-picker/demo-file-picker.adapter.ts: -------------------------------------------------------------------------------- 1 | import { FilePreviewModel } from './../../../projects/file-picker/src/lib/file-preview.model'; 2 | import { HttpRequest, HttpClient, HttpEvent, HttpEventType, HttpErrorResponse } from '@angular/common/http'; 3 | import { catchError, map } from 'rxjs/operators'; 4 | import { Observable, of } from 'rxjs'; 5 | import { FilePickerAdapter, UploadResponse, UploadStatus } from 'projects/file-picker/src/lib/file-picker.adapter'; 6 | 7 | export class DemoFilePickerAdapter extends FilePickerAdapter { 8 | constructor(private http: HttpClient) { 9 | super(); 10 | } 11 | public uploadFile(fileItem: FilePreviewModel): Observable { 12 | const form = new FormData(); 13 | form.append('file', fileItem.file); 14 | const api = 'https://ngx-awesome-uploader-2.free.beeceptor.com/upload'; 15 | const req = new HttpRequest('POST', api, form, {reportProgress: true}); 16 | return this.http.request(req) 17 | .pipe( 18 | // @ts-ignore: not all return value 19 | // TODO: fix return type 20 | map((res: HttpEvent) => { 21 | if (res.type === HttpEventType.Response) { 22 | const responseFromBackend = res.body; 23 | return { 24 | body: responseFromBackend, 25 | status: UploadStatus.UPLOADED 26 | }; 27 | } else if ((res.type === HttpEventType.UploadProgress) && res.total) { 28 | /** Compute and show the % done: */ 29 | const uploadProgress = +Math.round((100 * res.loaded) / res.total); 30 | return { 31 | status: UploadStatus.IN_PROGRESS, 32 | progress: uploadProgress 33 | }; 34 | } 35 | }), 36 | catchError((er: HttpErrorResponse) => { 37 | console.log(er); 38 | return of({status: UploadStatus.ERROR, body: er }); 39 | }) 40 | ); 41 | } 42 | public removeFile(fileItem: FilePreviewModel): Observable { 43 | const id = 50; 44 | const responseFromBackend = fileItem.uploadResponse; 45 | const removeApi = 'https://run.mocky.io/v3/dedf88ec-7ce8-429a-829b-bd2fc55352bc'; 46 | return this.http.post(removeApi, {id}); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/file-preview-item/file-preview-item.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 7 |
8 |
12 | {{fileType}} 13 |
14 |
15 | 16 |
17 |
18 |
19 |

{{fileItem.fileName}}

20 |
{{niceBytes(fileItem.file?.size)}}
21 |
22 |
23 |
24 | 25 |
26 | 27 | 31 | 32 | 33 |
34 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 | 44 |
45 |
{{uploadProgress}} %
46 |
47 |
48 | 49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags.ts'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /projects/file-picker/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "align": { 5 | "options": ["parameters", "statements"] 6 | }, 7 | "array-type": false, 8 | "arrow-return-shorthand": true, 9 | "curly": true, 10 | "deprecation": { 11 | "severity": "warning" 12 | }, 13 | "component-class-suffix": true, 14 | "contextual-lifecycle": true, 15 | "directive-class-suffix": true, 16 | "eofline": true, 17 | "import-blacklist": [true, "rxjs/Rx"], 18 | "import-spacing": true, 19 | "prefer-for-of": false, 20 | "indent": { 21 | "options": ["spaces"] 22 | }, 23 | "max-classes-per-file": false, 24 | "max-line-length": [true, 140], 25 | "member-ordering": [ 26 | true, 27 | { 28 | "order": ["static-field", "instance-field", "static-method", "instance-method"] 29 | } 30 | ], 31 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 32 | "no-empty": false, 33 | "no-inferrable-types": [true, "ignore-params"], 34 | "no-non-null-assertion": true, 35 | "no-redundant-jsdoc": true, 36 | "no-switch-case-fall-through": true, 37 | "no-var-requires": false, 38 | "object-literal-key-quotes": [true, "as-needed"], 39 | "quotemark": [true, "single", "avoid-escape"], 40 | "semicolon": { 41 | "options": ["always", "strict-bound-class-methods"] 42 | }, 43 | "space-before-function-paren": { 44 | "options": { 45 | "anonymous": "never", 46 | "asyncArrow": "always", 47 | "constructor": "never", 48 | "method": "never", 49 | "named": "never" 50 | } 51 | }, 52 | "typedef-whitespace": { 53 | "options": [ 54 | { 55 | "call-signature": "nospace", 56 | "index-signature": "nospace", 57 | "parameter": "nospace", 58 | "property-declaration": "nospace", 59 | "variable-declaration": "nospace" 60 | }, 61 | { 62 | "call-signature": "onespace", 63 | "index-signature": "onespace", 64 | "parameter": "onespace", 65 | "property-declaration": "onespace", 66 | "variable-declaration": "onespace" 67 | } 68 | ] 69 | }, 70 | "whitespace": { 71 | "options": ["check-branch", "check-decl", "check-operator", "check-separator", "check-type", "check-typecast"] 72 | }, 73 | "no-conflicting-lifecycle": true, 74 | "no-host-metadata-property": true, 75 | "no-input-rename": true, 76 | "no-inputs-metadata-property": true, 77 | "no-output-native": true, 78 | "no-output-on-prefix": false, 79 | "no-output-rename": true, 80 | "no-outputs-metadata-property": true, 81 | "template-banana-in-box": true, 82 | "template-no-negated-async": true, 83 | "use-lifecycle-interface": true, 84 | "use-pipe-transform-interface": true 85 | }, 86 | "rulesDirectory": ["codelyzer"] 87 | } 88 | -------------------------------------------------------------------------------- /src/app/demo-file-picker/demo-file-picker.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FilePickerComponent, 3 | ValidationError, 4 | FilePreviewModel, 5 | UploaderCaptions, 6 | } from 'ngx-awesome-uploader'; 7 | import { HttpClient, HttpErrorResponse } from '@angular/common/http'; 8 | import {DemoFilePickerAdapter} from './demo-file-picker.adapter'; 9 | import {Component, OnInit, ViewChild} from '@angular/core'; 10 | import {Observable, of} from 'rxjs'; 11 | import {delay } from 'rxjs/operators'; 12 | 13 | @Component({ 14 | selector: 'app-demo-file-picker', 15 | templateUrl: './demo-file-picker.component.html', 16 | styleUrls: ['./demo-file-picker.component.scss'], 17 | standalone: false 18 | }) 19 | export class DemoFilePickerComponent implements OnInit { 20 | @ViewChild('uploader', { static: true }) uploader: FilePickerComponent; 21 | public adapter = new DemoFilePickerAdapter(this.http); 22 | public myFiles: FilePreviewModel[] = []; 23 | public captions: UploaderCaptions = { 24 | dropzone: { 25 | title: 'Fayllari bura ata bilersiz', 26 | or: 'və yaxud', 27 | browse: 'Fayl seçin' 28 | }, 29 | cropper: { 30 | crop: 'Kəs', 31 | cancel: 'Imtina' 32 | }, 33 | previewCard: { 34 | remove: 'Sil', 35 | uploadError: 'Fayl yüklənmədi' 36 | } 37 | }; 38 | 39 | constructor(private http: HttpClient) { } 40 | 41 | public ngOnInit(): void { 42 | setTimeout(() => { 43 | const files = [ 44 | { 45 | fileName: 'My File 1 for edit.png', file: null as any 46 | }, 47 | { 48 | fileName: 'My File 2 for edit.xlsx', file: null as any 49 | } 50 | ] as FilePreviewModel[]; 51 | // this.uploader.setFiles(files); 52 | }, 1000); 53 | } 54 | 55 | public onValidationError(er: ValidationError): void { 56 | console.log('validationError', er); 57 | } 58 | 59 | public onUploadSuccess(res: FilePreviewModel): void { 60 | console.log('uploadSuccess', res); 61 | // console.log(this.myFiles) 62 | } 63 | 64 | public onUploadFail(er: HttpErrorResponse): void { 65 | console.log('uploadFail', er); 66 | } 67 | 68 | public onRemoveSuccess(res: FilePreviewModel): void { 69 | console.log('removeSuccess', res); 70 | } 71 | 72 | public onFileAdded(file: FilePreviewModel): void { 73 | console.log('fileAdded', file); 74 | this.myFiles.push(file); 75 | } 76 | 77 | public onFileRemoved(file: FilePreviewModel): void { 78 | console.log('fileRemoved', this.uploader.files); 79 | } 80 | 81 | public removeFile(): void { 82 | this.uploader.removeFileFromList(this.myFiles[0]); 83 | } 84 | 85 | public myCustomValidator(file: File): Observable { 86 | if (!file.name.includes('uploader')) { 87 | return of(true).pipe(delay(100)); 88 | } 89 | return of(false).pipe(delay(100)); 90 | } 91 | 92 | public clearAllFiles(): void { 93 | this.uploader.files = []; 94 | } 95 | 96 | public onRemoveFile(fileItem: FilePreviewModel): void { 97 | this.uploader.removeFile(fileItem); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 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-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "no-inputs-metadata-property": true, 121 | "no-outputs-metadata-property": true, 122 | "no-host-metadata-property": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-lifecycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-picker.component.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | :host { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | width: 100%; 9 | height: 100%; 10 | overflow: auto; 11 | max-width: 440px; 12 | border-radius: 6px; 13 | } 14 | .files-preview-wrapper { 15 | width: 100%; 16 | } 17 | #cropper-img { 18 | max-width: 60vw; 19 | display: none; 20 | } 21 | #cropper-img img { 22 | width: 100%; 23 | height: 100%; 24 | } 25 | .file-drop-wrapper { 26 | width: 100%; 27 | background: #fafbfd; 28 | padding-top: 20px; 29 | } 30 | 31 | .preview-container { 32 | display: flex; 33 | } 34 | 35 | .cropperJsOverlay { 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | position: fixed; 40 | z-index: 999; 41 | top: 0; 42 | left: 0; 43 | width: 100vw; 44 | height: 100vh; 45 | background: rgba(0, 0, 0, 0.32); 46 | } 47 | .cropperJsBox { 48 | display: flex; 49 | flex-direction: column; 50 | justify-content: center; 51 | align-items: center; 52 | max-height: calc(100vh - 38px - 50px); 53 | max-width: 90vw; 54 | .cropper-actions { 55 | display: flex; 56 | button { 57 | margin: 5px; 58 | padding: 12px 25px; 59 | border-radius: 6px; 60 | border: 0; 61 | cursor: pointer; 62 | } 63 | .cropSubmit { 64 | color: #ffffff; 65 | background: #16a085; 66 | } 67 | .cropCancel { 68 | // color: #ffffff; 69 | // background: #474787; 70 | } 71 | } 72 | } 73 | ::ng-deep.cropper img { 74 | max-height: 300px !important; 75 | } 76 | #images { 77 | display: flex; 78 | justify-content: center; 79 | width: 500px; 80 | overflow-x: auto; 81 | } 82 | #images .image { 83 | flex: 0 0 100px; 84 | margin: 0 2px; 85 | display: flex; 86 | flex-direction: column; 87 | align-items: flex-end; 88 | } 89 | 90 | #fileInput { 91 | display: none; 92 | } 93 | 94 | .uploader-submit-btn { 95 | background: #ffd740; 96 | color: rgba(0, 0, 0, 0.87); 97 | border: 0; 98 | padding: 0 16px; 99 | line-height: 36px; 100 | font-size: 15px; 101 | margin-top: 12px; 102 | border-radius: 4px; 103 | box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 104 | 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); 105 | cursor: pointer; 106 | } 107 | button:disabled { 108 | color: rgba(0,0,0,.26); 109 | background: gainsboro; 110 | } 111 | 112 | .visually-hidden { 113 | border: 0; 114 | clip: rect(0 0 0 0); 115 | height: 1px; 116 | margin: -1px; 117 | overflow: hidden; 118 | padding: 0; 119 | position: absolute; 120 | width: 1px; 121 | 122 | // Avoid browsers rendering the focus ring in some cases. 123 | outline: 0; 124 | 125 | // Avoid some cases where the browser will still render the native controls (see #9049). 126 | -webkit-appearance: none; 127 | -moz-appearance: none; 128 | } 129 | 130 | button.is-loading { 131 | color: rgba(0,0,0,.26)!important; 132 | background-color: #ffffff!important; 133 | box-shadow: none; 134 | cursor: not-allowed; 135 | outline: none; 136 | &:after { 137 | content: ""; 138 | font-family: sans-serif; 139 | font-weight: 100; 140 | -webkit-animation: 1.25s linear infinite three-quarters; 141 | animation: 1.25s linear infinite three-quarters; 142 | border: 3px solid #7f8c8d; 143 | border-right-color: transparent; 144 | border-radius: 100%; 145 | box-sizing: border-box; 146 | display: inline-block; 147 | position: relative; 148 | vertical-align: middle; 149 | overflow: hidden; 150 | text-indent: -9999px; 151 | width: 18px; 152 | height: 18px; 153 | opacity: 1; 154 | margin-left: 10px; 155 | } 156 | } 157 | @keyframes three-quarters { 158 | 0% { 159 | -webkit-transform: rotate(0deg); 160 | -moz-transform: rotate(0deg); 161 | -ms-transform: rotate(0deg); 162 | -o-transform: rotate(0deg); 163 | transform: rotate(0deg); 164 | } 165 | 166 | 100% { 167 | -webkit-transform: rotate(360deg); 168 | -moz-transform: rotate(360deg); 169 | -ms-transform: rotate(360deg); 170 | -o-transform: rotate(360deg); 171 | transform: rotate(360deg); 172 | } 173 | } -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-awesome-uploader": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:application", 19 | "options": { 20 | "outputPath": { 21 | "base": "dist/ngx-awesome-uploader" 22 | }, 23 | "index": "src/index.html", 24 | "polyfills": [ 25 | "src/polyfills.ts" 26 | ], 27 | "tsConfig": "tsconfig.app.json", 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets" 31 | ], 32 | "styles": [ 33 | "src/styles.scss" 34 | ], 35 | "scripts": [], 36 | "browser": "src/main.ts" 37 | }, 38 | "configurations": { 39 | "development": { 40 | "optimization": false, 41 | "extractLicenses": false, 42 | "sourceMap": true, 43 | "namedChunks": true 44 | }, 45 | "production": { 46 | "budgets": [ 47 | { 48 | "type": "anyComponentStyle", 49 | "maximumWarning": "6kb" 50 | } 51 | ], 52 | "fileReplacements": [ 53 | { 54 | "replace": "src/environments/environment.ts", 55 | "with": "src/environments/environment.prod.ts" 56 | } 57 | ], 58 | "outputHashing": "all" 59 | } 60 | }, 61 | "defaultConfiguration": "production" 62 | }, 63 | "serve": { 64 | "builder": "@angular-devkit/build-angular:dev-server", 65 | "options": { 66 | "buildTarget": "ngx-awesome-uploader:build" 67 | }, 68 | "configurations": { 69 | "production": { 70 | "buildTarget": "ngx-awesome-uploader:build:production" 71 | }, 72 | "development": { 73 | "buildTarget": "ngx-awesome-uploader:build:development" 74 | } 75 | }, 76 | "defaultConfiguration": "development" 77 | }, 78 | "extract-i18n": { 79 | "builder": "@angular-devkit/build-angular:extract-i18n", 80 | "options": { 81 | "buildTarget": "ngx-awesome-uploader:build" 82 | } 83 | }, 84 | "test": { 85 | "builder": "@angular-devkit/build-angular:karma", 86 | "options": { 87 | "main": "src/test.ts", 88 | "polyfills": "src/polyfills.ts", 89 | "tsConfig": "tsconfig.spec.json", 90 | "karmaConfig": "src/karma.conf.js", 91 | "styles": [ 92 | "src/styles.scss" 93 | ], 94 | "scripts": [], 95 | "assets": [ 96 | "src/favicon.ico", 97 | "src/assets" 98 | ] 99 | } 100 | } 101 | } 102 | }, 103 | "file-picker": { 104 | "root": "projects/file-picker", 105 | "sourceRoot": "projects/file-picker/src", 106 | "projectType": "library", 107 | "prefix": "ngx", 108 | "architect": { 109 | "build": { 110 | "builder": "@angular-devkit/build-angular:ng-packagr", 111 | "options": { 112 | "project": "projects/file-picker/ng-package.json" 113 | }, 114 | "configurations": { 115 | "production": { 116 | "tsConfig": "projects/file-picker/tsconfig.lib.prod.json" 117 | }, 118 | "development": { 119 | "tsConfig": "projects/file-picker/tsconfig.lib.json" 120 | } 121 | }, 122 | "defaultConfiguration": "production" 123 | }, 124 | "test": { 125 | "builder": "@angular-devkit/build-angular:karma", 126 | "options": { 127 | "main": "projects/file-picker/src/test.ts", 128 | "tsConfig": "projects/file-picker/tsconfig.spec.json", 129 | "karmaConfig": "projects/file-picker/karma.conf.js" 130 | } 131 | } 132 | } 133 | } 134 | }, 135 | "cli": { 136 | "analytics": false 137 | } 138 | } -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/file-preview-item/file-preview-item.component.ts: -------------------------------------------------------------------------------- 1 | import { FilePickerService } from './../../file-picker.service'; 2 | import { FilePreviewModel } from './../../file-preview.model'; 3 | import { Component, OnInit, Input, Output, EventEmitter, TemplateRef, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; 4 | import { SafeResourceUrl } from '@angular/platform-browser'; 5 | import { HttpErrorResponse } from '@angular/common/http'; 6 | import { GET_FILE_CATEGORY_TYPE, GET_FILE_TYPE, IS_IMAGE_FILE } from '../../file-upload.utils'; 7 | import { Subscription } from 'rxjs'; 8 | import { FilePickerAdapter, UploadResponse, UploadStatus } from '../../file-picker.adapter'; 9 | import { UploaderCaptions } from '../../uploader-captions'; 10 | 11 | @Component({ 12 | selector: 'file-preview-item', 13 | templateUrl: './file-preview-item.component.html', 14 | styleUrls: ['./file-preview-item.component.scss'], 15 | changeDetection: ChangeDetectionStrategy.OnPush, 16 | standalone: false 17 | }) 18 | export class FilePreviewItemComponent implements OnInit { 19 | @Output() public readonly removeFile = new EventEmitter(); 20 | @Output() public readonly uploadSuccess = new EventEmitter(); 21 | @Output() public readonly uploadFail = new EventEmitter(); 22 | @Output() public readonly imageClicked = new EventEmitter(); 23 | @Input() public fileItem: FilePreviewModel; 24 | @Input() adapter: FilePickerAdapter; 25 | @Input() itemTemplate: TemplateRef; 26 | @Input() captions: UploaderCaptions; 27 | @Input() enableAutoUpload: boolean; 28 | public uploadProgress: number; 29 | public isImageFile: boolean; 30 | public fileType: string; 31 | public safeUrl: SafeResourceUrl; 32 | public uploadError: boolean; 33 | public uploadResponse: any; 34 | private _uploadSubscription: Subscription; 35 | constructor( 36 | private fileService: FilePickerService, 37 | private changeRef: ChangeDetectorRef 38 | ) {} 39 | 40 | public ngOnInit() { 41 | if (this.fileItem.file) { 42 | this._uploadFile(this.fileItem); 43 | this.safeUrl = this.getSafeUrl(this.fileItem.file); 44 | } 45 | this.fileType = GET_FILE_TYPE(this.fileItem.fileName); 46 | this.isImageFile = IS_IMAGE_FILE(this.fileType); 47 | } 48 | 49 | public getSafeUrl(file: File | Blob): SafeResourceUrl { 50 | return this.fileService.createSafeUrl(file); 51 | } 52 | /** Converts bytes to nice size */ 53 | public niceBytes(x): string { 54 | const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 55 | let l = 0; 56 | let n = parseInt(x, 10) || 0; 57 | while (n >= 1024 && ++l) { 58 | n = n / 1024; 59 | } 60 | // include a decimal point and a tenths-place digit if presenting 61 | // less than ten of KB or greater units 62 | return n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]; 63 | } 64 | /** Retry file upload when upload was unsuccessfull */ 65 | public onRetry(): void { 66 | this.uploadError = undefined; 67 | this._uploadFile(this.fileItem); 68 | } 69 | 70 | public onRemove(fileItem: FilePreviewModel): void { 71 | this._uploadUnsubscribe(); 72 | this.removeFile.next({ 73 | ...fileItem, 74 | uploadResponse: this.uploadResponse || fileItem.uploadResponse 75 | }); 76 | } 77 | 78 | private _uploadFile(fileItem: FilePreviewModel): void { 79 | if (!this.enableAutoUpload) { 80 | return; 81 | } 82 | if (this.adapter) { 83 | this._uploadSubscription = 84 | this.adapter.uploadFile(fileItem) 85 | .subscribe((res: UploadResponse) => { 86 | if (res && res.status === UploadStatus.UPLOADED) { 87 | this._onUploadSuccess(res.body, fileItem); 88 | this.uploadProgress = undefined; 89 | } 90 | if (res && res.status === UploadStatus.IN_PROGRESS) { 91 | this.uploadProgress = res.progress; 92 | this.changeRef.detectChanges(); 93 | } 94 | if (res && res.status === UploadStatus.ERROR) { 95 | this.uploadError = true; 96 | this.uploadFail.next(res.body); 97 | this.uploadProgress = undefined; 98 | } 99 | this.changeRef.detectChanges(); 100 | }, (er: HttpErrorResponse) => { 101 | this.uploadError = true; 102 | this.uploadFail.next(er); 103 | this.uploadProgress = undefined; 104 | this.changeRef.detectChanges(); 105 | }); 106 | } else { 107 | console.warn('no adapter was provided'); 108 | } 109 | } 110 | /** Emits event when file upload api returns success */ 111 | private _onUploadSuccess(uploadResponse: any, fileItem: FilePreviewModel): void { 112 | this.uploadResponse = uploadResponse; 113 | this.fileItem.uploadResponse = uploadResponse; 114 | this.uploadSuccess.next({...fileItem, uploadResponse}); 115 | } 116 | 117 | /** Cancel upload. Cancels request */ 118 | private _uploadUnsubscribe(): void { 119 | if (this._uploadSubscription) { 120 | this._uploadSubscription.unsubscribe(); 121 | } 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/file-preview-item/file-preview-item.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | padding: 20px 16px; 4 | border-bottom: 1px solid #ebeef1; 5 | max-width: 440px; 6 | position: relative; 7 | } 8 | 9 | .visually-hidden { 10 | border: 0; 11 | clip: rect(0 0 0 0); 12 | height: 1px; 13 | margin: -1px; 14 | overflow: hidden; 15 | padding: 0; 16 | position: absolute; 17 | width: 1px; 18 | 19 | // Avoid browsers rendering the focus ring in some cases. 20 | outline: 0; 21 | 22 | // Avoid some cases where the browser will still render the native controls (see #9049). 23 | -webkit-appearance: none; 24 | -moz-appearance: none; 25 | } 26 | 27 | .file-preview-wrapper { 28 | display: flex; 29 | width: 100%; 30 | .file-preview-thumbnail { 31 | position: relative; 32 | z-index: 2; 33 | cursor: pointer; 34 | .img-preview-thumbnail { 35 | width: 36px; 36 | height: 36px; 37 | img { 38 | width: 100%; 39 | height: 100%; 40 | object-fit: cover; 41 | border-radius: 6px; 42 | } 43 | } 44 | .other-preview-thumbnail { 45 | width: 36px; 46 | height: 36px; 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | background: #706fd3; 51 | border-radius: 4px; 52 | color: #ffffff; 53 | font-size: 12px; 54 | font-weight: 700; 55 | white-space: nowrap; 56 | overflow: hidden; 57 | &.pdf { 58 | background: #e4394e; 59 | } 60 | &.doc, &.docx { 61 | background: #2196F3; 62 | } 63 | &.xls, &.xlsx { 64 | background: #4CAF50; 65 | } 66 | &.txt, &.ppt { 67 | background: #FF9800; 68 | } 69 | } 70 | 71 | .thumbnail-backdrop { 72 | visibility: hidden; 73 | position: absolute; 74 | left: 0; 75 | top:0; 76 | width: 100%; 77 | height: 100%; 78 | border-radius: 6px; 79 | transition: all 100ms ease-in-out; 80 | pointer-events: none; 81 | background: rgba(43,56,71,.2); 82 | } 83 | &:hover { 84 | .thumbnail-backdrop { 85 | visibility: visible; 86 | } 87 | } 88 | &:active { 89 | .thumbnail-backdrop { 90 | visibility: visible; 91 | background: rgba(43,56,71,.4); 92 | } 93 | } 94 | } 95 | .file-preview-description { 96 | display: flex; 97 | flex-direction: column; 98 | align-items: flex-start; 99 | padding-left: 16px; 100 | padding-right: 16px; 101 | color: #74809d; 102 | overflow: hidden; 103 | flex: 1; 104 | z-index: 2; 105 | position: relative; 106 | .file-preview-title { 107 | font-weight: 700; 108 | width: 90%; 109 | text-decoration: none; 110 | color: #74809d; 111 | cursor: default; 112 | p { 113 | text-overflow: ellipsis; 114 | max-width: 100%; 115 | overflow: hidden; 116 | white-space: nowrap; 117 | margin: 0; 118 | } 119 | } 120 | .file-preview-size { 121 | font-size: 12px; 122 | color: #979fb8; 123 | } 124 | } 125 | .file-preview-actions { 126 | display: flex; 127 | align-items: center; 128 | font-size: 10px; 129 | z-index: 3; 130 | position: relative; 131 | .ngx-checkmark-wrapper { 132 | position: relative; 133 | cursor: pointer; 134 | font-size: 22px; 135 | height: 20px; 136 | width: 20px; 137 | border-radius: 50%; 138 | background: #43d084; 139 | .ngx-checkmark { 140 | position: absolute; 141 | top: 0; 142 | left: 0; 143 | height: 19px; 144 | width: 19px; 145 | &:after { 146 | content: ''; 147 | position: absolute; 148 | display: block; 149 | left: 7px; 150 | top: 4px; 151 | width: 3px; 152 | height: 7px; 153 | border: 1px solid #ffffff; 154 | border-width: 0 3px 3px 0; 155 | -webkit-transform: rotate(45deg); 156 | -ms-transform: rotate(45deg); 157 | transform: rotate(45deg); 158 | } 159 | } 160 | } 161 | .ngx-close-icon-wrapper { 162 | border-radius: 50%; 163 | padding: 3px; 164 | margin-left: 5px; 165 | cursor: pointer; 166 | } 167 | } 168 | .file-upload-progress-bar-wrapper, 169 | .file-upload-percentage-wrapper { 170 | position: absolute; 171 | z-index: 1; 172 | width: 100%; 173 | height: 95%; 174 | left: 0; 175 | top: 0; 176 | bottom: 0; 177 | margin: auto; 178 | } 179 | .file-upload-progress-bar { 180 | background: #eef1fa; 181 | border-radius: 6px; 182 | width: 0%; 183 | height: 95%; 184 | transition: width 300ms ease-in; 185 | } 186 | .file-upload-percentage { 187 | padding-right: 10%; 188 | color: #c2cdda; 189 | padding-top: 5%; 190 | font-size: 19px; 191 | text-align: right; 192 | } 193 | .file-upload-error-wrapper { 194 | position: absolute; 195 | z-index: 1; 196 | width: 100%; 197 | height: 95%; 198 | left: 0; 199 | top: 0; 200 | bottom: 0; 201 | margin: auto; 202 | background: rgba(254,84,111,.06); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-drop/file-drop.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | Output, 5 | EventEmitter, 6 | NgZone, 7 | OnDestroy, 8 | Renderer2 9 | } from '@angular/core'; 10 | import { timer, Subscription } from 'rxjs'; 11 | 12 | import { UploadFile } from './upload-file.model'; 13 | import { UploadEvent } from './upload-event.model'; 14 | import { 15 | FileSystemFileEntry, 16 | FileSystemEntry, 17 | FileSystemDirectoryEntry 18 | } from './dom.types'; 19 | import { UploaderCaptions } from '../uploader-captions'; 20 | 21 | @Component({ 22 | selector: 'file-drop', 23 | templateUrl: './file-drop.component.html', 24 | styleUrls: ['./file-drop.component.scss'], 25 | standalone: false 26 | }) 27 | export class FileComponent implements OnDestroy { 28 | @Input() 29 | captions: UploaderCaptions; 30 | @Input() 31 | customstyle: string = null; 32 | @Input() 33 | disableIf = false; 34 | 35 | @Output() 36 | public onFileDrop: EventEmitter = new EventEmitter(); 37 | @Output() 38 | public onFileOver: EventEmitter = new EventEmitter(); 39 | @Output() 40 | public onFileLeave: EventEmitter = new EventEmitter(); 41 | 42 | stack = []; 43 | files: UploadFile[] = []; 44 | subscription: Subscription; 45 | dragoverflag = false; 46 | 47 | globalDisable = false; 48 | globalStart: () => void; 49 | globalEnd: () => void; 50 | 51 | numOfActiveReadEntries = 0; 52 | constructor(private zone: NgZone, private renderer: Renderer2) { 53 | if (!this.customstyle) { 54 | this.customstyle = 'drop-zone'; 55 | } 56 | this.globalStart = this.renderer.listen('document', 'dragstart', evt => { 57 | this.globalDisable = true; 58 | }); 59 | this.globalEnd = this.renderer.listen('document', 'dragend', evt => { 60 | this.globalDisable = false; 61 | }); 62 | } 63 | public onDragOver(event: Event): void { 64 | if (!this.globalDisable && !this.disableIf) { 65 | if (!this.dragoverflag) { 66 | this.dragoverflag = true; 67 | this.onFileOver.emit(event); 68 | } 69 | this.preventAndStop(event); 70 | } 71 | } 72 | 73 | public onDragLeave(event: Event): void { 74 | if (!this.globalDisable && !this.disableIf) { 75 | if (this.dragoverflag) { 76 | this.dragoverflag = false; 77 | this.onFileLeave.emit(event); 78 | } 79 | this.preventAndStop(event); 80 | } 81 | } 82 | 83 | dropFiles(event: any) { 84 | if (!this.globalDisable && !this.disableIf) { 85 | this.dragoverflag = false; 86 | event.dataTransfer.dropEffect = 'copy'; 87 | let length; 88 | if (event.dataTransfer.items) { 89 | length = event.dataTransfer.items.length; 90 | } else { 91 | length = event.dataTransfer.files.length; 92 | } 93 | 94 | for (let i = 0; i < length; i++) { 95 | let entry: FileSystemEntry; 96 | if (event.dataTransfer.items) { 97 | if (event.dataTransfer.items[i].webkitGetAsEntry) { 98 | entry = event.dataTransfer.items[i].webkitGetAsEntry(); 99 | } 100 | } else { 101 | if (event.dataTransfer.files[i].webkitGetAsEntry) { 102 | entry = event.dataTransfer.files[i].webkitGetAsEntry(); 103 | } 104 | } 105 | if (!entry) { 106 | const file: File = event.dataTransfer.files[i]; 107 | if (file) { 108 | const fakeFileEntry: FileSystemFileEntry = { 109 | name: file.name, 110 | isDirectory: false, 111 | isFile: true, 112 | file: (callback: (filea: File) => void): void => { 113 | callback(file); 114 | } 115 | }; 116 | const toUpload: UploadFile = new UploadFile( 117 | fakeFileEntry.name, 118 | fakeFileEntry 119 | ); 120 | this.addToQueue(toUpload); 121 | } 122 | } else { 123 | if (entry.isFile) { 124 | const toUpload: UploadFile = new UploadFile(entry.name, entry); 125 | this.addToQueue(toUpload); 126 | } else if (entry.isDirectory) { 127 | this.traverseFileTree(entry, entry.name); 128 | } 129 | } 130 | } 131 | 132 | this.preventAndStop(event); 133 | 134 | const timerObservable = timer(200, 200); 135 | this.subscription = timerObservable.subscribe(t => { 136 | if (this.files.length > 0 && this.numOfActiveReadEntries === 0) { 137 | this.onFileDrop.emit(new UploadEvent(this.files)); 138 | this.files = []; 139 | } 140 | }); 141 | } 142 | } 143 | 144 | private traverseFileTree(item: FileSystemEntry, path: string) { 145 | if (item.isFile) { 146 | const toUpload: UploadFile = new UploadFile(path, item); 147 | this.files.push(toUpload); 148 | this.zone.run(() => { 149 | this.popToStack(); 150 | }); 151 | } else { 152 | this.pushToStack(path); 153 | path = path + '/'; 154 | const dirReader = (item as FileSystemDirectoryEntry).createReader(); 155 | let entries = []; 156 | const thisObj = this; 157 | 158 | const readEntries = () => { 159 | thisObj.numOfActiveReadEntries++; 160 | dirReader.readEntries((res) => { 161 | if (!res.length) { 162 | // add empty folders 163 | if (entries.length === 0) { 164 | const toUpload: UploadFile = new UploadFile(path, item); 165 | thisObj.zone.run(() => { 166 | thisObj.addToQueue(toUpload); 167 | }); 168 | } else { 169 | for (let i = 0; i < entries.length; i++) { 170 | thisObj.zone.run(() => { 171 | thisObj.traverseFileTree(entries[i], path + entries[i].name); 172 | }); 173 | } 174 | } 175 | thisObj.zone.run(() => { 176 | thisObj.popToStack(); 177 | }); 178 | } else { 179 | // continue with the reading 180 | entries = entries.concat(res); 181 | readEntries(); 182 | } 183 | thisObj.numOfActiveReadEntries--; 184 | }); 185 | }; 186 | 187 | readEntries(); 188 | } 189 | } 190 | 191 | private addToQueue(item: UploadFile) { 192 | this.files.push(item); 193 | } 194 | 195 | pushToStack(str) { 196 | this.stack.push(str); 197 | } 198 | 199 | popToStack() { 200 | const value = this.stack.pop(); 201 | } 202 | 203 | private clearQueue() { 204 | this.files = []; 205 | } 206 | 207 | private preventAndStop(event) { 208 | event.stopPropagation(); 209 | event.preventDefault(); 210 | } 211 | 212 | ngOnDestroy() { 213 | if (this.subscription) { 214 | this.subscription.unsubscribe(); 215 | } 216 | this.globalStart(); 217 | this.globalEnd(); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-preview-container/file-preview-item/file-preview-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { Observable, of } from 'rxjs'; 4 | import { FilePickerAdapter, UploadResponse, UploadStatus } from '../../file-picker.adapter'; 5 | import { FilePickerService } from '../../file-picker.service'; 6 | import { FilePreviewModel } from '../../file-preview.model'; 7 | import { createMockPreviewFile } from '../../test-utils'; 8 | import { FilePreviewItemComponent } from './file-preview-item.component'; 9 | 10 | class MockUploaderAdapter extends FilePickerAdapter { 11 | public uploadFile(fileItem: FilePreviewModel): Observable { 12 | return of({body: 50, status: UploadStatus.UPLOADED}); 13 | } 14 | public removeFile(fileItem: FilePreviewModel) { 15 | return of('ok'); 16 | } 17 | } 18 | 19 | describe('FilePreviewComponent', () => { 20 | let component: FilePreviewItemComponent; 21 | let fixture: ComponentFixture; 22 | beforeEach((() => { 23 | TestBed.configureTestingModule({ 24 | declarations: [ 25 | FilePreviewItemComponent 26 | ], 27 | providers: [FilePickerService], 28 | schemas: [NO_ERRORS_SCHEMA] 29 | }).compileComponents(); 30 | })); 31 | 32 | beforeEach(() => { 33 | fixture = TestBed.createComponent(FilePreviewItemComponent); 34 | component = fixture.componentInstance; 35 | component.fileItem = createMockPreviewFile('test.png', 'image/', 10); 36 | component.uploadSuccess.next = jasmine.createSpy('uploadSuccess'); 37 | component.uploadFail.next = jasmine.createSpy('uploadFail'); 38 | component.removeFile.next = jasmine.createSpy('removeFile'); 39 | }); 40 | 41 | it('should create the app', () => { 42 | const app = fixture.componentInstance; 43 | expect(app).toBeTruthy(); 44 | }); 45 | 46 | it('should itemTemplate be undefined by default', () => { 47 | expect(component.itemTemplate).toBeUndefined(); 48 | }); 49 | 50 | it('should captions be undefined by default', () => { 51 | expect(component.captions).toBeUndefined(); 52 | }); 53 | 54 | it('should adapter be undefined by default', () => { 55 | expect(component.adapter).toBeUndefined(); 56 | }); 57 | 58 | it('should enableAutoUpload be undefined by default', () => { 59 | expect(component.enableAutoUpload).toBeUndefined(); 60 | }); 61 | 62 | it('should uploadProgress be undefined by default', () => { 63 | expect(component.uploadProgress).toBeUndefined(); 64 | }); 65 | it('should fileType be undefined by default', () => { 66 | expect(component.fileType).toBeUndefined(); 67 | }); 68 | 69 | it('should safeUrl be undefined by default', () => { 70 | expect(component.safeUrl).toBeUndefined(); 71 | }); 72 | 73 | it('should uploadError be undefined by default', () => { 74 | expect(component.uploadError).toBeUndefined(); 75 | }); 76 | 77 | it('should uploadResponse be undefined by default', () => { 78 | expect(component.uploadResponse).toBeUndefined(); 79 | }); 80 | 81 | describe('#ngOnInit', () => { 82 | 83 | it('should safeUrl be defined', () => { 84 | component.ngOnInit(); 85 | expect(component.safeUrl).toBeDefined(); 86 | }); 87 | 88 | it('should fileType be defined', () => { 89 | component.ngOnInit(); 90 | expect(component.fileType).toEqual('PNG'); 91 | }); 92 | 93 | describe('and uploadFile', () => { 94 | describe('and enableAutoUpload is true', () => { 95 | beforeEach(() => { 96 | component.enableAutoUpload = true; 97 | }); 98 | 99 | describe('and adapter exist', () => { 100 | beforeEach(() => { 101 | component.adapter = new MockUploaderAdapter(); 102 | spyOn(component.adapter, 'uploadFile'); 103 | (component.adapter.uploadFile as any).and.returnValue(of({body: 10, status: UploadStatus.UPLOADED})); 104 | }); 105 | it('should upload file', () => { 106 | component.ngOnInit(); 107 | expect(component.adapter.uploadFile).toHaveBeenCalled(); 108 | }); 109 | describe('and upload resposne type is UPLOADED', () => { 110 | let uploadResponse; 111 | beforeEach(() => { 112 | uploadResponse = {id: 12}; 113 | (component.adapter.uploadFile as any).and.returnValue(of({body: uploadResponse, status: UploadStatus.UPLOADED})); 114 | }); 115 | it('should uploadResponse be defined', () => { 116 | component.ngOnInit(); 117 | expect(component.uploadResponse).toEqual(uploadResponse); 118 | }); 119 | 120 | it('should uploadProgress be undefined', () => { 121 | component.ngOnInit(); 122 | expect(component.uploadProgress).toBeUndefined(); 123 | }); 124 | 125 | it('should emit uploadSuccess', () => { 126 | component.ngOnInit(); 127 | expect(component.uploadSuccess.next).toHaveBeenCalledWith({ 128 | ...component.fileItem, 129 | uploadResponse 130 | }); 131 | }); 132 | }); 133 | 134 | describe('and upload resposne type is IN_PROGRESS', () => { 135 | beforeEach(() => { 136 | (component.adapter.uploadFile as any).and.returnValue(of({progress: 10, status: UploadStatus.IN_PROGRESS})); 137 | }); 138 | it('should uploadProgress be defined', () => { 139 | component.ngOnInit(); 140 | expect(component.uploadProgress).toBe(10); 141 | }); 142 | }); 143 | 144 | describe('and upload resposne type is ERROR', () => { 145 | let error; 146 | beforeEach(() => { 147 | error = 'some-error'; 148 | (component.adapter.uploadFile as any).and.returnValue(of({body: error, status: UploadStatus.ERROR})); 149 | }); 150 | it('should uploadError be true', () => { 151 | component.ngOnInit(); 152 | expect(component.uploadError).toBe(true); 153 | }); 154 | it('should uploadProgress be undefined', () => { 155 | component.ngOnInit(); 156 | expect(component.uploadProgress).toBeUndefined(); 157 | }); 158 | it('should uploadFail be emitted', () => { 159 | component.ngOnInit(); 160 | expect(component.uploadFail.next).toHaveBeenCalledWith(error); 161 | }); 162 | }); 163 | }); 164 | }); 165 | 166 | describe('and enableAutoUpload is false', () => { 167 | beforeEach(() => { 168 | component.enableAutoUpload = false; 169 | }); 170 | 171 | describe('and adapter exist', () => { 172 | beforeEach(() => { 173 | component.adapter = new MockUploaderAdapter(); 174 | spyOn(component.adapter, 'uploadFile'); 175 | (component.adapter.uploadFile as any).and.returnValue(of({body: 10, status: UploadStatus.UPLOADED})); 176 | }); 177 | it('should NOT upload file', () => { 178 | component.ngOnInit(); 179 | expect(component.adapter.uploadFile).not.toHaveBeenCalled(); 180 | }); 181 | }); 182 | }); 183 | }); 184 | }); 185 | 186 | describe('#onRemove', () => { 187 | beforeEach(() => { 188 | component.uploadResponse = {id: 10 }; 189 | }); 190 | it('should removeFile be emitted', () => { 191 | component.onRemove(component.fileItem); 192 | expect(component.removeFile.next).toHaveBeenCalledWith( 193 | {...component.fileItem, uploadResponse: component.uploadResponse} 194 | ); 195 | }); 196 | }); 197 | 198 | }); 199 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # NGX-AWESOME-UPLOADER 3 | 4 | ![alt-text](https://raw.githubusercontent.com/vugar005/ngx-awesome-uploader/master/angular-image.gif?raw=true) 5 | 6 | 7 | 8 | [![npm](https://img.shields.io/npm/l/ngx-awesome-uploader.svg)]() [![NPM Downloads](https://img.shields.io/npm/dt/ngx-awesome-uploader.svg)](https://www.npmjs.com/package/ngx-awesome-uploader) [![npm demo](https://img.shields.io/badge/demo-online-ed1c46.svg)](https://stackblitz.com/edit/ngx-awesome-uploader?file=src%2Fapp%2Fsimple-demo%2Fsimple-demo.component.ts) [![npm](https://img.shields.io/twitter/follow/vugar005.svg?label=Follow)](https://twitter.com/vugar005) [![npm](https://img.shields.io/github/issues/vugar005/ngx-awesome-uploader.svg)](https://github.com/vugar005/ngx-awesome-uploader) [![npm](https://img.shields.io/github/last-commit/vugar005/ngx-awesome-uploader.svg)](https://github.com/vugar005/ngx-awesome-uploader) ![npm](https://img.shields.io/readthedocs/ngx-awesome-uploader.svg) 9 | 10 | 11 | 12 | 13 | 14 | This is an Angular Library for uploading files. It supports: File Upload and Preview (additionally preview images with lightbox), validation, image cropper , drag and drop with multi language support. 15 | 16 | 17 | 18 | Tested on Angular 6+. Supports Server Side Rendering. 19 | >**Breaking Changes:** [Check Changes](https://github.com/vugar005/ngx-awesome-uploader/blob/master/breaking-changes-v10.md) changes if you come from version < 10. 20 | 21 | 22 | * [Install](#install) 23 | * [Usage](#usage) 24 | * [Configuration](#api) 25 | * [File Validation](#file-validation) 26 | * [Built-in validations](#built-in-validations) 27 | * [Custom validation](#custom-validation) 28 | * [Cropper](#cropper) 29 | * [Custom template](#custom-template) 30 | * [Multi Language](#multi-language) 31 | * [Edit Mode](#edit-mode) 32 | * [Bonus](#bonus) 33 | 34 | 35 | 36 | ## Quick-links 37 | 38 | [Example Application](https://ngx-awesome-uploader.stackblitz.io/) or 39 | 40 | [StackBlitzDemo](https://stackblitz.com/edit/ngx-awesome-uploader?file=src%2Fapp%2Fsimple-demo%2Fsimple-demo.component.ts) 41 | 42 | ## Install 43 | 44 | 45 | 46 | npm install ngx-awesome-uploader --save 47 | 48 | 49 | 50 | ##### Load the module for your app: 51 | 52 | 53 | 54 | ```typescript 55 | import { FilePickerModule } from 'ngx-awesome-uploader'; 56 | 57 | @NgModule({ 58 | imports: [ 59 | ... 60 | FilePickerModule 61 | ] 62 | }) 63 | 64 | ``` 65 | 66 | ## Usage 67 | 68 | In order to make library maximum compatible with apis you need to create and provide 69 | custom adapter which implements upload and remove requests. That's because I have no idea how to get file id in upload response json :) . 70 | So this libray exposes a FilePickerAdapter abstract class which you can import on your new class file definition: 71 | 72 | 73 | ``` import { FilePickerAdapter } from 'ngx-awesome-uploader';``` 74 | 75 | 76 | 77 | After importing it to your custom adapter implementation (EG: CustomAdapter.ts), you must implement those 2 methods which are abstract in the FilePickerAdapter base class which are: 78 | 79 | 80 | 81 | ``` 82 | public abstract uploadFile(fileItem: FilePreviewModel): Observable; 83 | 84 | public abstract removeFile(fileItem: FilePreviewModel): Observable; 85 | ``` 86 | 87 | 88 | 89 | You can check DEMO adapter [here](https://github.com/vugar005/ngx-awesome-uploader/tree/master/projects/file-picker/src/lib/mock-file-picker.adapter.ts) 90 | 91 | 92 | 93 | #### Now you can use it in your template 94 | 95 | 96 | 97 | ```html 98 | 101 | 102 | 103 | ``` 104 | 105 | 106 | 107 | #### and in the Component: 108 | 109 | 110 | 111 | ```typescript 112 | import { HttpClient } from '@angular/common/http'; 113 | import { DemoFilePickerAdapter } from './demo-file-picker.adapter'; 114 | import { Component} from '@angular/core'; 115 | 116 | @Component({ 117 | selector: 'demo-file-picker', 118 | templateUrl: './demo-file-picker.component.html', 119 | styleUrls: ['./demo-file-picker.component.scss'] 120 | }) 121 | 122 | export class DemoFilePickerComponent { 123 | adapter = new DemoFilePickerAdapter(this.http); 124 | constructor(private http: HttpClient) { } 125 | } 126 | 127 | ``` 128 | 129 | >**Note:** As you see you should provide http instance to adapter. 130 | 131 | Still in Doubt? Check [Minimal Setup Demo](https://stackblitz.com/edit/ngx-awesome-uploader?file=src%2Fapp%2Fsimple-demo%2Fsimple-demo.component.ts) 132 | 133 | 134 | 135 | ## API 136 | 137 | ```typescript 138 | 139 | /** Whether to enable cropper. Default: disabled */ 140 | @Input() enableCropper = false; 141 | 142 | /** Whether to show default drag and drop template. Default:true */ 143 | @Input() showeDragDropZone = true; 144 | 145 | /** Single or multiple. Default: multi */ 146 | @Input() uploadType = 'multi'; 147 | 148 | /** Max size of selected file in MB. Default: no limit */ 149 | @Input() fileMaxSize: number; 150 | 151 | /** Max count of file in multi-upload. Default: no limit */ 152 | @Input() fileMaxCount: number; 153 | 154 | /** Total Max size limit of all files in MB. Default: no limit */ 155 | @Input() totalMaxSize: number; 156 | 157 | /** Which file types to show on choose file dialog. Default: show all */ 158 | @Input() accept: string; 159 | 160 | /** File extensions filter. Default: any exteion */ 161 | @Input() fileExtensions: String; 162 | 163 | /** Cropper options if cropper enabled. Default: 164 | dragMode: 'crop', 165 | aspectRatio: 1, 166 | autoCrop: true, 167 | movable: true, 168 | zoomable: true, 169 | scalable: true, 170 | autoCropArea: 0.8 171 | */ 172 | @Input() cropperOptions: Object; 173 | 174 | /** Custom Adapter for uploading/removing files. Required */ 175 | @Input() adapter: FilePickerAdapter; 176 | 177 | /** Custom template for dropzone. Optional */ 178 | @Input() dropzoneTemplate: TemplateRef; 179 | 180 | /** Custom Preview Item template. Optional */ 181 | @Input() itemTemplate: TemplateRef; 182 | 183 | /** Whether to show default files preview container. Default: true */ 184 | @Input() showPreviewContainer = true; 185 | 186 | /** Custom validator function. Optional */ 187 | @Input() customValidator: (file: File) => Observable; 188 | 189 | /** Custom captions input. Used for multi language support */ 190 | @Input() captions: UploaderCaptions; 191 | 192 | /** Whether to auto upload file on file choose or not. Default: true. You can get files list by accessing component files. */ 193 | @Input() enableAutoUpload = true; 194 | 195 | /** capture paramerter for file input such as user,environment*/ 196 | @Input() fileInputCapture: string; 197 | 198 | ``` 199 | 200 | ## Output events 201 | 202 | 203 | 204 | ```typescript 205 | 206 | /** Emitted when file upload via api success. 207 | Emitted for every file */ 208 | @Output() uploadSuccess = new EventEmitter(); 209 | 210 | /** Emitted when file upload via api fails. 211 | Emitted for every file */ 212 | @Output() uploadFail = new EventEmitter(); 213 | 214 | /** Emitted when file is removed via api successfully. 215 | Emitted for every file */ 216 | @Output() removeSuccess = new EventEmitter(); 217 | 218 | /** Emitted on file validation fail */ 219 | @Output() validationError = new EventEmitter(); 220 | 221 | /** Emitted when file is added and passed validations. Not uploaded yet */ 222 | @Output() fileAdded = new EventEmitter(); 223 | 224 | /** Emitted when file is removed from fileList */ 225 | @Output() fileRemoved = new EventEmitter(); 226 | ``` 227 | 228 | 229 | 230 | ## File-Validation 231 | 232 | ### Built-in-validations 233 | 234 | All validations are emitted through ValidationError event. 235 | 236 | To listen to validation errors (in case you provided validations), validationError event is emitted. validationError event implements interface [ValidationError](https://github.com/vugar005/ngx-awesome-uploader/blob/master/projects/file-picker/src/lib/validation-error.model.ts) 237 | and which emits failed file and error type. 238 | 239 | Supported validations: 240 | 241 | | **Validation Type** | **Description** | **Default** | 242 | |----------------------------|---------------------------------------------------------------------------------------|----------------------------------------| 243 | | fileMaxSize: number | Max size of selected file in MB. | No limit 244 | | fileExtensions: String | Emitted when file does not satisfy provided extension | Any extension 245 | | uploadType: String | Upload type. Values: 'single' and 'multi'. |multi 246 | | totalMaxSize: number | Total Max size of files in MB. If cropper is enabled, the cropped image size is considered.| No limit 247 | | fileMaxCount: number | Limit total files to upload by count | No limit 248 | 249 | 250 | ### Custom-validation 251 | 252 | You can also provide your own custom validation along with built-in validations. 253 | 254 | You custom validation takes `file: File` and returns `Observable`; 255 | 256 | So that means you can provide sync and async validations. 257 | 258 | 259 | 260 | ``` 261 | public myCustomValidator(file: File): Observable { 262 | if (file.name.includes('panda')) { 263 | return of(true); 264 | } 265 | 266 | if (file.size > 50) { 267 | return this.http.get('url').pipe(map((res) => res === 'OK' )); 268 | } 269 | return of(false); 270 | } 271 | ``` 272 | and pass to Template: 273 | 274 | ```html 275 | 278 | 279 | 280 | ``` 281 | 282 | 283 | Check [Demo](https://stackblitz.com/edit/ngx-awesome-uploader?file=src%2Fapp%2Fadvanced-demo%2Fadvanced-demo.component.html) 284 | 285 | 286 | 287 | 288 | 289 | ## Cropper 290 | 291 | 292 | 293 | Library uses cropperjs to crop images but you need import it to use it. Example: in index html 294 | 295 | 296 | 297 | ```html 298 | 299 | 300 | 301 | 302 | ``` 303 | 304 | 305 | 306 | >**Note**: To use cropper, you should set enableCropper to true. Look at API section above. 307 | 308 | You can also provide your custom cropper options. 309 | 310 | 311 | 312 | ## Custom-Template 313 | 314 | You can provide custom template to library. 315 | 316 | I) To provide custom template for drag and drop zone, use content projection. Example: 317 | 318 | ```html 319 | 321 | 322 |
323 | 324 |
325 | 326 |
327 | 328 | ```` 329 | 330 | 331 | 332 | >**Note:** The wrapper of your custom template must have a class **dropzoneTemplate**. 333 | 334 | 335 | 336 | [Checkout Demo](https://stackblitz.com/edit/ngx-awesome-uploader?file=src%2Fapp%2Fadvanced-demo%2Fadvanced-demo.component.html) 337 | 338 | 339 | 340 | II) To use custom file preview template, pass your custom template as below: 341 | 342 | 343 | 344 | ```html 345 | 346 | 350 | 351 | 352 | 353 | 354 |

{{fileItem.file.size}}

355 | 356 |

{{fileItem.fileName}}

357 | 358 |

{{uploadProgress}}%

359 | 360 | 361 | 362 |
363 | 364 | ``` 365 | 366 | In custom template uploadProgress and fileItem (which implements [FilePrevieModel](https://github.com/vugar005/ngx-awesome-uploader/blob/master/projects/file-picker/src/lib/file-preview.model.ts) interface) are exposed . 367 | 368 | ## Multi Language 369 | 370 | You can add multi language support for library by providing ***captions*** object (which implements [UploaderCaptions](https://github.com/vugar005/ngx-awesome-uploader/blob/master/projects/file-picker/src/lib/uploader-captions.ts) interface). 371 | 372 | 373 | 374 | Check [Demo](https://stackblitz.com/edit/ngx-awesome-uploader?file=src%2Fapp%2Fadvanced-demo%2Fadvanced-demo.component.html) 375 | 376 | ## Edit Mode 377 | 378 | You can show your files without uploading them 379 | 380 | ``` @ViewChild('uploader', { static: true }) uploader: FilePickerComponent; ``` 381 | 382 | ``` 383 | public ngOnInit(): void { 384 | const files = [ 385 | { 386 | fileName: 'My File 1 for edit.png' 387 | }, 388 | { 389 | fileName: 'My File 2 for edit.xlsx' 390 | } 391 | ] as FilePreviewModel[]; 392 | this.uploader.setFiles(files); 393 | } 394 | ``` 395 | ## Bonus 396 | 397 | You can also check out library [router animations ](https://www.npmjs.com/package/ngx-router-animations) 398 | 399 | ## Contribution 400 | 401 | 402 | 403 | You can fork project from github. Pull requests are kindly accepted. 404 | 405 | 1. Building library: ng build file-picker --prod 406 | 407 | 2. Running tests: ng test file-picker --browsers=ChromeHeadless 408 | 409 | 3. Run demo: ng serve -------------------------------------------------------------------------------- /projects/file-picker/README.md: -------------------------------------------------------------------------------- 1 | 2 | # NGX-AWESOME-UPLOADER 3 | 4 | 5 | ![alt-text](https://raw.githubusercontent.com/vugar005/ngx-awesome-uploader/master/angular-image.gif?raw=true) 6 | 7 | 8 | 9 | [![npm](https://img.shields.io/npm/l/ngx-awesome-uploader.svg)]() [![NPM Downloads](https://img.shields.io/npm/dt/ngx-awesome-uploader.svg)](https://www.npmjs.com/package/ngx-awesome-uploader) [![npm demo](https://img.shields.io/badge/demo-online-ed1c46.svg)](https://stackblitz.com/edit/ngx-awesome-uploader?file=src%2Fapp%2Fsimple-demo%2Fsimple-demo.component.ts) [![npm](https://img.shields.io/twitter/follow/vugar005.svg?label=Follow)](https://twitter.com/vugar005) [![npm](https://img.shields.io/github/issues/vugar005/ngx-awesome-uploader.svg)](https://github.com/vugar005/ngx-awesome-uploader) [![npm](https://img.shields.io/github/last-commit/vugar005/ngx-awesome-uploader.svg)](https://github.com/vugar005/ngx-awesome-uploader) ![npm](https://img.shields.io/readthedocs/ngx-awesome-uploader.svg) 10 | 11 | 12 | 13 | 14 | 15 | This is an Angular Library for uploading files. It supports: File Upload and Preview (additionally preview images with lightbox), validation, image cropper , drag and drop with multi language support. 16 | 17 | 18 | 19 | Tested on Angular Angular 6+. Supports Server Side Rendering. 20 | >**Breaking Changes:** [Check Changes](https://github.com/vugar005/ngx-awesome-uploader/blob/master/breaking-changes-v10.md) changes if you come from version < 10. 21 | 22 | 23 | * [Install](#install) 24 | * [Usage](#usage) 25 | * [Configuration](#api) 26 | * [File Validation](#file-validation) 27 | * [Built-in validations](#built-in-validations) 28 | * [Custom validation](#custom-validation) 29 | * [Cropper](#cropper) 30 | * [Custom template](#custom-template) 31 | * [Multi Language](#multi-language) 32 | * [Edit Mode](#edit-mode) 33 | * [Bonus](#bonus) 34 | 35 | 36 | 37 | ## Quick-links 38 | 39 | [Example Application](https://ngx-awesome-uploader.stackblitz.io/) or 40 | 41 | [StackBlitzDemo](https://stackblitz.com/edit/ngx-awesome-uploader?file=src%2Fapp%2Fsimple-demo%2Fsimple-demo.component.ts) 42 | 43 | ## Install 44 | 45 | 46 | 47 | npm install ngx-awesome-uploader --save 48 | 49 | 50 | 51 | ##### Load the module for your app: 52 | 53 | 54 | 55 | ```typescript 56 | import { FilePickerModule } from 'ngx-awesome-uploader'; 57 | 58 | @NgModule({ 59 | imports: [ 60 | ... 61 | FilePickerModule 62 | ] 63 | }) 64 | 65 | ``` 66 | 67 | ## Usage 68 | 69 | In order to make library maximum compatible with apis you need to create and provide 70 | custom adapter which implements upload and remove requests. That's because I have no idea how to get file id in upload response json :) . 71 | So this libray exposes a FilePickerAdapter abstract class which you can import on your new class file definition: 72 | 73 | 74 | ``` import { FilePickerAdapter } from 'ngx-awesome-uploader';``` 75 | 76 | 77 | 78 | After importing it to your custom adapter implementation (EG: CustomAdapter.ts), you must implement those 2 methods which are abstract in the FilePickerAdapter base class which are: 79 | 80 | 81 | 82 | ``` 83 | public abstract uploadFile(fileItem: FilePreviewModel): Observable; 84 | 85 | public abstract removeFile(fileItem: FilePreviewModel): Observable; 86 | ``` 87 | 88 | 89 | 90 | You can check DEMO adapter [here](https://github.com/vugar005/ngx-awesome-uploader/tree/master/projects/file-picker/src/lib/mock-file-picker.adapter.ts) 91 | 92 | 93 | 94 | #### Now you can use it in your template 95 | 96 | 97 | 98 | ```html 99 | 102 | 103 | 104 | ``` 105 | 106 | 107 | 108 | #### and in the Component: 109 | 110 | 111 | 112 | ```typescript 113 | import { HttpClient } from '@angular/common/http'; 114 | import { DemoFilePickerAdapter } from './demo-file-picker.adapter'; 115 | import { Component} from '@angular/core'; 116 | 117 | @Component({ 118 | selector: 'demo-file-picker', 119 | templateUrl: './demo-file-picker.component.html', 120 | styleUrls: ['./demo-file-picker.component.scss'] 121 | }) 122 | 123 | export class DemoFilePickerComponent { 124 | adapter = new DemoFilePickerAdapter(this.http); 125 | constructor(private http: HttpClient) { } 126 | } 127 | 128 | ``` 129 | 130 | >**Note:** As you see you should provide http instance to adapter. 131 | 132 | Still in Doubt? Check [Minimal Setup Demo](https://stackblitz.com/edit/ngx-awesome-uploader?file=src%2Fapp%2Fsimple-demo%2Fsimple-demo.component.ts) 133 | 134 | 135 | 136 | ## API 137 | 138 | ```typescript 139 | 140 | /** Whether to enable cropper. Default: disabled */ 141 | @Input() enableCropper = false; 142 | 143 | /** Whether to show default drag and drop template. Default:true */ 144 | @Input() showeDragDropZone = true; 145 | 146 | /** Single or multiple. Default: multi */ 147 | @Input() uploadType = 'multi'; 148 | 149 | /** Max size of selected file in MB. Default: no limit */ 150 | @Input() fileMaxSize: number; 151 | 152 | /** Max count of file in multi-upload. Default: no limit */ 153 | @Input() fileMaxCount: number; 154 | 155 | /** Total Max size limit of all files in MB. Default: no limit */ 156 | @Input() totalMaxSize: number; 157 | 158 | /** Which file types to show on choose file dialog. Default: show all */ 159 | @Input() accept: string; 160 | 161 | /** File extensions filter. Default: any exteion */ 162 | @Input() fileExtensions: String; 163 | 164 | /** Cropper options if cropper enabled. Default: 165 | dragMode: 'crop', 166 | aspectRatio: 1, 167 | autoCrop: true, 168 | movable: true, 169 | zoomable: true, 170 | scalable: true, 171 | autoCropArea: 0.8 172 | */ 173 | @Input() cropperOptions: Object; 174 | 175 | /** Custom Adapter for uploading/removing files. Required */ 176 | @Input() adapter: FilePickerAdapter; 177 | 178 | /** Custom template for dropzone. Optional */ 179 | @Input() dropzoneTemplate: TemplateRef; 180 | 181 | /** Custom Preview Item template. Optional */ 182 | @Input() itemTemplate: TemplateRef; 183 | 184 | /** Whether to show default files preview container. Default: true */ 185 | @Input() showPreviewContainer = true; 186 | 187 | /** Custom validator function. Optional */ 188 | @Input() customValidator: (file: File) => Observable; 189 | 190 | /** Custom captions input. Used for multi language support */ 191 | @Input() captions: UploaderCaptions; 192 | 193 | /** Whether to auto upload file on file choose or not. Default: true. You can get files list by accessing component files. */ 194 | @Input() enableAutoUpload = true; 195 | 196 | /** capture paramerter for file input such as user,environment*/ 197 | @Input() fileInputCapture: string; 198 | 199 | ``` 200 | 201 | ## Output events 202 | 203 | 204 | 205 | ```typescript 206 | 207 | /** Emitted when file upload via api success. 208 | Emitted for every file */ 209 | @Output() uploadSuccess = new EventEmitter(); 210 | 211 | /** Emitted when file upload via api fails. 212 | Emitted for every file */ 213 | @Output() uploadFail = new EventEmitter(); 214 | 215 | /** Emitted when file is removed via api successfully. 216 | Emitted for every file */ 217 | @Output() removeSuccess = new EventEmitter(); 218 | 219 | /** Emitted on file validation fail */ 220 | @Output() validationError = new EventEmitter(); 221 | 222 | /** Emitted when file is added and passed validations. Not uploaded yet */ 223 | @Output() fileAdded = new EventEmitter(); 224 | 225 | /** Emitted when file is removed from fileList */ 226 | @Output() fileRemoved = new EventEmitter(); 227 | ``` 228 | 229 | 230 | 231 | ## File-Validation 232 | 233 | ### Built-in-validations 234 | 235 | All validations are emitted through ValidationError event. 236 | 237 | To listen to validation errors (in case you provided validations), validationError event is emitted. validationError event implements interface [ValidationError](https://github.com/vugar005/ngx-awesome-uploader/blob/master/projects/file-picker/src/lib/validation-error.model.ts) 238 | and which emits failed file and error type. 239 | 240 | Supported validations: 241 | 242 | | **Validation Type** | **Description** | **Default** | 243 | |----------------------------|---------------------------------------------------------------------------------------|----------------------------------------| 244 | | fileMaxSize: number | Max size of selected file in MB. | No limit 245 | | fileExtensions: String | Emitted when file does not satisfy provided extension | Any extension 246 | | uploadType: String | Upload type. Values: 'single' and 'multi'. |multi 247 | | totalMaxSize: number | Total Max size of files in MB. If cropper is enabled, the cropped image size is considered.| No limit 248 | | fileMaxCount: number | Limit total files to upload by count | No limit 249 | 250 | 251 | ### Custom-validation 252 | 253 | You can also provide your own custom validation along with built-in validations. 254 | 255 | You custom validation takes `file: File` and returns `Observable`; 256 | 257 | So that means you can provide sync and async validations. 258 | 259 | 260 | 261 | ``` 262 | public myCustomValidator(file: File): Observable { 263 | if (file.name.includes('panda')) { 264 | return of(true); 265 | } 266 | 267 | if (file.size > 50) { 268 | return this.http.get('url').pipe(map((res) => res === 'OK' )); 269 | } 270 | return of(false); 271 | } 272 | ``` 273 | and pass to Template: 274 | 275 | ```html 276 | 279 | 280 | 281 | ``` 282 | 283 | 284 | Check [Demo](https://stackblitz.com/edit/ngx-awesome-uploader?file=src%2Fapp%2Fadvanced-demo%2Fadvanced-demo.component.html) 285 | 286 | 287 | 288 | 289 | 290 | ## Cropper 291 | 292 | 293 | 294 | Library uses cropperjs to crop images but you need import it to use it. Example: in index html 295 | 296 | 297 | 298 | ```html 299 | 300 | 301 | 302 | 303 | ``` 304 | 305 | 306 | 307 | >**Note**: To use cropper, you should set enableCropper to true. Look at API section above. 308 | 309 | You can also provide your custom cropper options. 310 | 311 | 312 | 313 | ## Custom-Template 314 | 315 | You can provide custom template to library. 316 | 317 | I) To provide custom template for drag and drop zone, use content projection. Example: 318 | 319 | ```html 320 | 322 | 323 |
324 | 325 |
326 | 327 |
328 | 329 | ```` 330 | 331 | 332 | 333 | >**Note:** The wrapper of your custom template must have a class **dropzoneTemplate**. 334 | 335 | 336 | 337 | [Checkout Demo](https://stackblitz.com/edit/ngx-awesome-uploader?file=src%2Fapp%2Fadvanced-demo%2Fadvanced-demo.component.html) 338 | 339 | 340 | 341 | II) To use custom file preview template, pass your custom template as below: 342 | 343 | 344 | 345 | ```html 346 | 347 | 351 | 352 | 353 | 354 | 355 |

{{fileItem.file.size}}

356 | 357 |

{{fileItem.fileName}}

358 | 359 |

{{uploadProgress}}%

360 | 361 | 362 | 363 |
364 | 365 | ``` 366 | 367 | In custom template uploadProgress and fileItem (which implements [FilePrevieModel](https://github.com/vugar005/ngx-awesome-uploader/blob/master/projects/file-picker/src/lib/file-preview.model.ts) interface) are exposed . 368 | 369 | ## Multi Language 370 | 371 | You can add multi language support for library by providing ***captions*** object (which implements [UploaderCaptions](https://github.com/vugar005/ngx-awesome-uploader/blob/master/projects/file-picker/src/lib/uploader-captions.ts) interface). 372 | 373 | 374 | 375 | Check [Demo](https://stackblitz.com/edit/ngx-awesome-uploader?file=src%2Fapp%2Fadvanced-demo%2Fadvanced-demo.component.html) 376 | 377 | ## Edit Mode 378 | 379 | You can show your files without uploading them 380 | 381 | ``` @ViewChild('uploader', { static: true }) uploader: FilePickerComponent; ``` 382 | 383 | ``` 384 | public ngOnInit(): void { 385 | const files = [ 386 | { 387 | fileName: 'My File 1 for edit.png' 388 | }, 389 | { 390 | fileName: 'My File 2 for edit.xlsx' 391 | } 392 | ] as FilePreviewModel[]; 393 | this.uploader.setFiles(files); 394 | } 395 | ``` 396 | ## Bonus 397 | 398 | You can also check out library [router animations ](https://www.npmjs.com/package/ngx-router-animations) 399 | 400 | ## Contribution 401 | 402 | 403 | 404 | You can fork project from github. Pull requests are kindly accepted. 405 | 406 | 1. Building library: ng build file-picker --prod 407 | 408 | 2. Running tests: ng test file-picker --browsers=ChromeHeadless 409 | 410 | 3. Run demo: ng serve -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-picker.spec.ts: -------------------------------------------------------------------------------- 1 | import { FilePickerAdapter, UploadResponse, UploadStatus } from 'projects/file-picker/src/lib/file-picker.adapter'; 2 | import { FilePreviewContainerComponent } from './file-preview-container/file-preview-container.component'; 3 | import { FileValidationTypes } from './validation-error.model'; 4 | import { FilePickerService } from './file-picker.service'; 5 | import { FilePickerModule, FilePreviewModel } from 'projects/file-picker/src/public_api'; 6 | import { ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing'; 7 | 8 | import { FilePickerComponent } from './file-picker.component'; 9 | import { createMockFile, createMockPreviewFile, mockCustomValidator } from './test-utils'; 10 | import { Observable, of } from 'rxjs'; 11 | import { By } from '@angular/platform-browser'; 12 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 13 | import { DEFAULT_CROPPER_OPTIONS } from './file-picker.constants'; 14 | 15 | export class MockableUploaderAdapter extends FilePickerAdapter { 16 | public uploadFile(fileItem: FilePreviewModel): Observable { 17 | return of({body: 50, status: UploadStatus.UPLOADED}); 18 | } 19 | public removeFile(fileItem: FilePreviewModel) { 20 | return of('ok'); 21 | } 22 | } 23 | describe('FilePickerComponent', () => { 24 | let component: FilePickerComponent; 25 | let fixture: ComponentFixture; 26 | let service: FilePickerService; 27 | let mockFile: File; 28 | let mockFilePreview: FilePreviewModel; 29 | (window as any).UPLOADER_TEST_MODE = true; 30 | beforeEach((() => { 31 | TestBed.configureTestingModule({ 32 | declarations: [ ], 33 | imports: [FilePickerModule], 34 | schemas: [NO_ERRORS_SCHEMA], 35 | providers: [FilePickerService] 36 | }) 37 | .compileComponents(); 38 | })); 39 | 40 | beforeEach(() => { 41 | fixture = TestBed.createComponent(FilePickerComponent); 42 | component = fixture.componentInstance; 43 | service = new FilePickerService(null); 44 | fixture.detectChanges(); 45 | mockFile = createMockFile('demo.pdf', 'application/pdf'); 46 | mockFilePreview = createMockPreviewFile('demo.pdf', 'application/pdf'); 47 | component.fileAdded.next = jasmine.createSpy('fileAdded'); 48 | component.validationError.next = jasmine.createSpy('validationError'); 49 | }); 50 | 51 | it('should enableCropper be false by default', () => { 52 | component.ngOnInit(); 53 | expect(component.enableCropper).toBe(false); 54 | }); 55 | 56 | it('should showDragDropZone be true by default', () => { 57 | component.ngOnInit(); 58 | expect(component.showeDragDropZone).toBe(true); 59 | }); 60 | 61 | it('should showPreviewContainer be true by default', () => { 62 | component.ngOnInit(); 63 | expect(component.showPreviewContainer).toBe(true); 64 | }); 65 | 66 | it('should uploadType be multi by default', () => { 67 | component.ngOnInit(); 68 | expect(component.uploadType).toBe('multi'); 69 | }); 70 | 71 | it('should itemTemplate be undefined by default', () => { 72 | component.ngOnInit(); 73 | expect(component.itemTemplate).toBeUndefined(); 74 | }); 75 | 76 | it('should fileMaxSize be undefined by default', () => { 77 | component.ngOnInit(); 78 | expect(component.fileMaxSize).toBeUndefined(); 79 | }); 80 | 81 | it('should fileMaxCount be undefined by default', () => { 82 | component.ngOnInit(); 83 | expect(component.fileMaxCount).toBeUndefined(); 84 | }); 85 | 86 | it('should totalMaxSize be undefined by default', () => { 87 | component.ngOnInit(); 88 | expect(component.totalMaxSize).toBeUndefined(); 89 | }); 90 | 91 | it('should enableAutoUpload be true by default', () => { 92 | component.ngOnInit(); 93 | expect(component.enableAutoUpload).toBe(true); 94 | }); 95 | 96 | it('should use default cropper options when not provided', () => { 97 | component.ngOnInit(); 98 | fixture.detectChanges(); 99 | expect(component.cropperOptions).toEqual(DEFAULT_CROPPER_OPTIONS); 100 | }); 101 | 102 | describe('#onChange', () => { 103 | describe('and isValidMaxFileCount', () => { 104 | let file1: File; 105 | let file2: File; 106 | beforeEach(() => { 107 | file1 = createMockFile('demo.png', 'image/png', 5); 108 | file2 = createMockFile('demo-2.png', 'image/png', 10); 109 | component.fileMaxCount = 2; 110 | }); 111 | describe('and isValidUploadType true', () => { 112 | beforeEach(() => { 113 | component.uploadType = 'multi'; 114 | }); 115 | 116 | describe('and isValidExtension true', () => { 117 | beforeEach(() => { 118 | component.fileExtensions = ['png']; 119 | }); 120 | 121 | describe('and async validations valid', () => { 122 | beforeEach(() => { 123 | component.customValidator = () => of(true); 124 | }); 125 | 126 | describe('and cropper is disabled', () => { 127 | beforeEach(() => { 128 | component.enableCropper = false; 129 | }); 130 | describe('and isValidSize', () => { 131 | beforeEach(() => { 132 | component.fileMaxSize = 50; 133 | }); 134 | it('should push files to fileList', () => { 135 | const files = [ file1, file2 ] as File[]; 136 | component.onChange(files); 137 | expect(component.files).toEqual([ 138 | {file: file1, fileName: file1.name}, 139 | {file: file2, fileName: file2.name} 140 | ]); 141 | }); 142 | 143 | it('should fileAdded emit file', () => { 144 | const files = [ file1 ] as File[]; 145 | component.onChange(files); 146 | expect(component.fileAdded.next).toHaveBeenCalledWith({file: file1, fileName: file1.name}); 147 | }); 148 | 149 | it('should NOT emit validationError with size error', () => { 150 | const files = [ file1 ] as File[]; 151 | component.onChange(files); 152 | expect(component.validationError.next).not.toHaveBeenCalled(); 153 | }); 154 | }); 155 | describe('and NOT isValidSize', () => { 156 | beforeEach(() => { 157 | file1 = createMockFile('demo.png', 'image/png', 10); 158 | component.fileMaxSize = 6; 159 | }); 160 | it('should NOT push files to fileList', () => { 161 | const files = [ file1 ] as File[]; 162 | component.onChange(files); 163 | expect(component.files).toEqual([]); 164 | }); 165 | 166 | it('should fileAdded NOT emit file', () => { 167 | const files = [ file1 ] as File[]; 168 | component.onChange(files); 169 | expect(component.fileAdded.next).not.toHaveBeenCalled(); 170 | }); 171 | 172 | it('should emit validationError with size error', () => { 173 | const files = [ file1 ] as File[]; 174 | component.onChange(files); 175 | expect(component.validationError.next).toHaveBeenCalledWith({ 176 | file: file1, 177 | error: FileValidationTypes.fileMaxSize 178 | }); 179 | }); 180 | 181 | }); 182 | }); 183 | 184 | describe('and cropper is enabled', () => { 185 | beforeEach(() => { 186 | component.enableCropper = true; 187 | component.currentCropperFile = undefined; 188 | (window as any).CROPPER = true; 189 | }); 190 | it('should filesForCropper be defined', () => { 191 | const files = [ file1 ] as File[]; 192 | component.onChange(files); 193 | expect(component.filesForCropper).toEqual([file1]); 194 | }); 195 | 196 | it('should set currentCropperFile', () => { 197 | const files = [ file1 ] as File[]; 198 | component.onChange(files); 199 | expect(component.currentCropperFile).toEqual(file1); 200 | }); 201 | 202 | it('should set safeCropImgUrl', () => { 203 | const files = [ file1 ] as File[]; 204 | component.onChange(files); 205 | expect(component.safeCropImgUrl).toBeDefined(); 206 | }); 207 | }); 208 | }); 209 | }); 210 | 211 | describe('and isValidExtension false', () => { 212 | beforeEach(() => { 213 | component.fileExtensions = ['ttgt']; 214 | }); 215 | 216 | describe('and async validations valid', () => { 217 | beforeEach(() => { 218 | component.customValidator = () => of(true); 219 | }); 220 | 221 | describe('and cropper is disabled', () => { 222 | beforeEach(() => { 223 | component.enableCropper = false; 224 | }); 225 | describe('and isValidSize be true', () => { 226 | beforeEach(() => { 227 | component.fileMaxSize = 50; 228 | }); 229 | it('should NOT push files to fileList', () => { 230 | const files = [ file1, file2 ] as File[]; 231 | component.onChange(files); 232 | expect(component.files).toEqual([]); 233 | }); 234 | 235 | it('should fileAdded NOT emit file', () => { 236 | const files = [ file1 ] as File[]; 237 | component.onChange(files); 238 | expect(component.fileAdded.next).not.toHaveBeenCalled(); 239 | }); 240 | 241 | }); 242 | describe('and NOT isValidSize', () => { 243 | beforeEach(() => { 244 | file1 = createMockFile('demo.png', 'image/png', 10); 245 | component.fileMaxSize = 6; 246 | }); 247 | it('should NOT push files to fileList', () => { 248 | const files = [ file1 ] as File[]; 249 | component.onChange(files); 250 | expect(component.files).toEqual([]); 251 | }); 252 | 253 | it('should fileAdded NOT emit file', () => { 254 | const files = [ file1 ] as File[]; 255 | component.onChange(files); 256 | expect(component.fileAdded.next).not.toHaveBeenCalled(); 257 | }); 258 | }); 259 | }); 260 | 261 | describe('and cropper is enabled', () => { 262 | beforeEach(() => { 263 | component.enableCropper = true; 264 | component.currentCropperFile = undefined; 265 | (window as any).CROPPER = true; 266 | }); 267 | it('should filesForCropper NOT be defined', () => { 268 | const files = [ file1 ] as File[]; 269 | component.onChange(files); 270 | expect(component.filesForCropper).toEqual([]); 271 | }); 272 | 273 | it('should NOT set currentCropperFile', () => { 274 | const files = [ file1 ] as File[]; 275 | component.onChange(files); 276 | expect(component.currentCropperFile).not.toBeDefined(); 277 | }); 278 | 279 | it('should NOT set safeCropImgUrl', () => { 280 | const files = [ file1 ] as File[]; 281 | component.onChange(files); 282 | expect(component.safeCropImgUrl).not.toBeDefined(); 283 | }); 284 | }); 285 | }); 286 | }); 287 | }); 288 | }); 289 | }); 290 | 291 | it('should open cropper when type is image and cropper enabled', async () => { 292 | const spy = spyOn(component, 'openCropper'); 293 | component.enableCropper = true; 294 | const file = createMockFile('demo.png', 'image/png'); 295 | await component.handleFiles([file]).toPromise(); 296 | expect(spy).toHaveBeenCalled(); 297 | }); 298 | 299 | it('should NOT open cropper when type is not image', async () => { 300 | const spy = spyOn(component, 'openCropper'); 301 | component.enableCropper = true; 302 | const file = createMockFile('demo.pdf', 'application/pdf'); 303 | await component.handleFiles([file]).toPromise(); 304 | expect(spy).not.toHaveBeenCalled(); 305 | }); 306 | 307 | it('should NOT open cropper when cropper is not enabled', async () => { 308 | const spy = spyOn(component, 'openCropper'); 309 | component.enableCropper = false; 310 | const file = createMockFile('demo.png', 'image/png'); 311 | await component.handleFiles([file]).toPromise(); 312 | expect(spy).not.toHaveBeenCalled(); 313 | }); 314 | 315 | it('should NOT push file on size validation fail without cropper feature', () => { 316 | const spy = spyOn(component, 'pushFile'); 317 | component.fileMaxSize = 1; 318 | const file = createMockFile('demo.png', 'image/png', 1.1); 319 | component.handleFiles([file]).toPromise(); 320 | expect(spy).not.toHaveBeenCalled(); 321 | }); 322 | 323 | it('should NOT push file on fileMaxCount validation fail', () => { 324 | const spy = spyOn(component, 'pushFile'); 325 | component.fileMaxCount = 1; 326 | component.files.push(createMockPreviewFile('demo.png', 'image/png')); 327 | expect(spy).not.toHaveBeenCalled(); 328 | }); 329 | it('should NOT push another file when upload type is single', async () => { 330 | const spy = spyOn(component, 'pushFile'); 331 | component.uploadType = 'single'; 332 | component.files.push(createMockPreviewFile('demo.png', 'image/png')); 333 | const file = createMockFile('demo2.png', 'image/png'); 334 | await component.handleFiles([file]).toPromise(); 335 | expect(spy).not.toHaveBeenCalled(); 336 | }); 337 | 338 | it('should push images to filesForCropper if cropper Enabled', fakeAsync(async () => { 339 | component.enableCropper = true; 340 | const files = [createMockFile('test.jpg', 'image/jpeg'), createMockFile('test2.png', 'image/png')]; 341 | await component.handleFiles(files).toPromise(); 342 | expect(component.filesForCropper.length).toBe(2); 343 | })); 344 | 345 | xit('should open cropper as many times as image length on multi mode', fakeAsync(async () => { 346 | spyOn(component, 'openCropper').and.callThrough(); 347 | spyOn(component, 'closeCropper').and.callThrough(); 348 | component.enableCropper = true; 349 | const files = [createMockFile('test.jpg', 'image/jpeg'), createMockFile('test2.png', 'image/png')]; 350 | await component.handleFiles(files).toPromise(); 351 | fixture.detectChanges(); 352 | fixture.debugElement.query(By.css('.cropCancel')).nativeElement.click(); 353 | tick(1000); 354 | fixture.whenStable().then(res => { 355 | fixture.detectChanges(); 356 | expect(component.openCropper).toHaveBeenCalledTimes(2); 357 | }); 358 | })); 359 | 360 | }); 361 | -------------------------------------------------------------------------------- /projects/file-picker/src/lib/file-picker.component.ts: -------------------------------------------------------------------------------- 1 | import { FilePickerService } from './file-picker.service'; 2 | import { 3 | ChangeDetectionStrategy, 4 | ChangeDetectorRef, 5 | Component, 6 | EventEmitter, inject, Injector, 7 | Input, 8 | OnDestroy, 9 | OnInit, 10 | Output, runInInjectionContext, 11 | TemplateRef 12 | } from '@angular/core'; 13 | import { SafeResourceUrl } from '@angular/platform-browser'; 14 | import { FilePreviewModel } from './file-preview.model'; 15 | import { GET_FILE_CATEGORY_TYPE } from './file-upload.utils'; 16 | import { FileValidationTypes, ValidationError } from './validation-error.model'; 17 | import { FilePickerAdapter } from './file-picker.adapter'; 18 | import { 19 | FileSystemDirectoryEntry, 20 | FileSystemFileEntry, 21 | UploadEvent 22 | } from './file-drop'; 23 | import { bufferCount, combineLatest, Observable, of, Subject, switchMap} from 'rxjs'; 24 | import { map, takeUntil, tap } from 'rxjs/operators'; 25 | import { DefaultCaptions } from './default-captions'; 26 | import { UploaderCaptions } from './uploader-captions'; 27 | import { HttpErrorResponse } from '@angular/common/http'; 28 | import { DEFAULT_CROPPER_OPTIONS } from './file-picker.constants'; 29 | import { lookup } from 'mrmime'; 30 | import { FileValidatorService } from './services/file-validator/file-validator.service'; 31 | import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; 32 | 33 | 34 | declare var Cropper; 35 | @Component({ 36 | selector: 'ngx-awesome-uploader', 37 | templateUrl: './file-picker.component.html', 38 | styleUrls: ['./file-picker.component.scss'], 39 | changeDetection: ChangeDetectionStrategy.OnPush, 40 | standalone: false 41 | }) 42 | export class FilePickerComponent implements OnInit, OnDestroy { 43 | /** Emitted when file upload via api successfully. Emitted for every file */ 44 | @Output() readonly uploadSuccess = new EventEmitter(); 45 | /** Emitted when file upload via api failed. Emitted for every file */ 46 | @Output() readonly uploadFail = new EventEmitter(); 47 | /** Emitted when file is removed via api successfully. Emitted for every file */ 48 | @Output() readonly removeSuccess = new EventEmitter(); 49 | /** Emitted on file validation fail */ 50 | @Output() readonly validationError = new EventEmitter(); 51 | /** Emitted when file is added and passed validations. Not uploaded yet */ 52 | @Output() readonly fileAdded = new EventEmitter(); 53 | /** Emitted when file is removed from fileList */ 54 | @Output() readonly fileRemoved = new EventEmitter(); 55 | /** Custom validator function */ 56 | @Input() customValidator: (file: File) => Observable; 57 | /** Whether to enable cropper. Default: disabled */ 58 | @Input() enableCropper = false; 59 | /** Whether to show default drag and drop zone. Default:true */ 60 | @Input() showeDragDropZone = true; 61 | /** Whether to show default files preview container. Default: true */ 62 | @Input() showPreviewContainer = true; 63 | /** Preview Item template */ 64 | @Input() itemTemplate: TemplateRef; 65 | /** Single or multiple. Default: multi */ 66 | @Input() uploadType = 'multi'; 67 | /** Max size of selected file in MB. Default: no limit */ 68 | @Input() fileMaxSize: number; 69 | /** Max count of file in multi-upload. Default: no limit */ 70 | @Input() fileMaxCount: number; 71 | /** Total Max size limit of all files in MB. Default: no limit */ 72 | @Input() totalMaxSize: number; 73 | /** Which file types to show on choose file dialog. Default: show all */ 74 | @Input() accept: string; 75 | /** File extensions filter */ 76 | @Input() fileExtensions: string[]; 77 | /** Cropper options. */ 78 | @Input() cropperOptions: object; 79 | /** Cropped canvas options. */ 80 | @Input() croppedCanvasOptions: object = {}; 81 | /** Custom api Adapter for uploading/removing files */ 82 | @Input() adapter: FilePickerAdapter; 83 | /** Custome template for dropzone */ 84 | @Input() dropzoneTemplate: TemplateRef; 85 | /** Custom captions input. Used for multi language support */ 86 | @Input() captions: UploaderCaptions = DefaultCaptions; 87 | /** captions object */ 88 | /** Whether to auto upload file on file choose or not. Default: true */ 89 | @Input() enableAutoUpload = true; 90 | 91 | /** capture paramerter for file input such as user,environment*/ 92 | @Input() fileInputCapture: string; 93 | 94 | cropper: any; 95 | public files: FilePreviewModel[] = []; 96 | /** Files array for cropper. Will be shown equentially if crop enabled */ 97 | filesForCropper: File[] = []; 98 | /** Current file to be shown in cropper */ 99 | public currentCropperFile: File; 100 | public safeCropImgUrl: SafeResourceUrl; 101 | public isCroppingBusy: boolean; 102 | 103 | private _cropClosed$ = new Subject(); 104 | private _onDestroy$ = new Subject(); 105 | private readonly injector = inject(Injector); 106 | 107 | constructor( 108 | private readonly fileService: FilePickerService, 109 | private readonly fileValidatorService: FileValidatorService, 110 | private readonly changeRef: ChangeDetectorRef 111 | ) {} 112 | 113 | public ngOnInit(): void { 114 | this._setCropperOptions(); 115 | this._listenToCropClose(); 116 | } 117 | 118 | public ngOnDestroy(): void { 119 | this._onDestroy$.next(); 120 | this._onDestroy$.complete(); 121 | } 122 | 123 | /** On input file selected */ 124 | // TODO: fix any 125 | public onChange(event: any): void { 126 | const files: File[] = Array.from(event); 127 | this.handleFiles(files).subscribe(); 128 | } 129 | 130 | /** On file dropped */ 131 | public dropped(event: UploadEvent): void { 132 | const files = event.files; 133 | const filesForUpload: Subject = new Subject(); 134 | let droppedFilesCount = 0; 135 | for (const droppedFile of files) { 136 | // Is it a file? 137 | if (droppedFile.fileEntry.isFile) { 138 | droppedFilesCount += 1; 139 | const fileEntry = droppedFile.fileEntry as FileSystemFileEntry; 140 | fileEntry.file((file: File) => { 141 | filesForUpload.next(file); 142 | }); 143 | } else { 144 | // It was a directory (empty directories are added, otherwise only files) 145 | const fileEntry = droppedFile.fileEntry as FileSystemDirectoryEntry; 146 | // console.log(droppedFile.relativePath, fileEntry); 147 | } 148 | } 149 | runInInjectionContext((this.injector), () => { 150 | filesForUpload.pipe( 151 | takeUntilDestroyed(), 152 | bufferCount(droppedFilesCount), 153 | switchMap(filesForUpload => this.handleFiles(filesForUpload)) 154 | ).subscribe(); 155 | }); 156 | } 157 | 158 | /** Emits event when file upload api returns success */ 159 | public onUploadSuccess(fileItem: FilePreviewModel): void { 160 | this.uploadSuccess.next(fileItem); 161 | } 162 | 163 | /** Emits event when file upload api returns success */ 164 | public onUploadFail(er: HttpErrorResponse): void { 165 | this.uploadFail.next(er); 166 | } 167 | 168 | /** Emits event when file remove api returns success */ 169 | public onRemoveSuccess(fileItem: FilePreviewModel): void { 170 | this.removeSuccess.next(fileItem); 171 | this.removeFileFromList(fileItem); 172 | } 173 | 174 | public getSafeUrl(file: File): SafeResourceUrl { 175 | return this.fileService.createSafeUrl(file); 176 | } 177 | 178 | /** Removes file from UI and sends api */ 179 | public removeFile(fileItem: FilePreviewModel): void { 180 | if (!this.enableAutoUpload) { 181 | this.removeFileFromList(fileItem); 182 | return; 183 | } 184 | if (this.adapter) { 185 | this.adapter.removeFile(fileItem).subscribe(res => { 186 | this.onRemoveSuccess(fileItem); 187 | }); 188 | } else { 189 | console.warn('no adapter was provided'); 190 | } 191 | } 192 | 193 | /** Listen when Cropper is closed and open new cropper if next image exists */ 194 | private _listenToCropClose(): void { 195 | this._cropClosed$ 196 | .pipe(takeUntil(this._onDestroy$)) 197 | .subscribe((res: FilePreviewModel) => { 198 | const croppedIndex = this.filesForCropper.findIndex( 199 | item => item.name === res.fileName 200 | ); 201 | const nextFile = 202 | croppedIndex !== -1 203 | ? this.filesForCropper[croppedIndex + 1] 204 | : undefined; 205 | this.filesForCropper = [...this.filesForCropper].filter( 206 | item => item.name !== res.fileName 207 | ); 208 | if (nextFile) { 209 | this.openCropper(nextFile); 210 | } 211 | }); 212 | } 213 | 214 | /** Sets custom cropper options if avaiable */ 215 | private _setCropperOptions(): void { 216 | if (!this.cropperOptions) { 217 | this._setDefaultCropperOptions(); 218 | } 219 | } 220 | /** Sets manual cropper options if no custom options are avaiable */ 221 | private _setDefaultCropperOptions(): void { 222 | this.cropperOptions = DEFAULT_CROPPER_OPTIONS; 223 | } 224 | 225 | /** Handles input and drag/drop files */ 226 | handleFiles(files: File[]): Observable { 227 | if (!this.isValidMaxFileCount(files)) { 228 | return of(null); 229 | } 230 | const isValidUploadSync = files.every(item => this._validateFileSync(item)); 231 | const asyncFunctions = files.map(item => this._validateFileAsync(item)); 232 | return combineLatest([...asyncFunctions]).pipe( 233 | map(res => { 234 | const isValidUploadAsync = res.every(result => result === true); 235 | if (!isValidUploadSync || !isValidUploadAsync) { 236 | return; 237 | } 238 | files.forEach((file: File, index: number) => { 239 | this.handleInputFile(file, index); 240 | }); 241 | }) 242 | ); 243 | } 244 | 245 | /** Validates synchronous validations */ 246 | private _validateFileSync(file: File): boolean { 247 | if (!file) { 248 | return false; 249 | } 250 | if (!this.isValidUploadType(file)) { 251 | return false; 252 | } 253 | if (!this.isValidExtension(file, file.name)) { 254 | return false; 255 | } 256 | return true; 257 | } 258 | 259 | /** Validates asynchronous validations */ 260 | private _validateFileAsync(file: File): Observable { 261 | if (!this.customValidator) { 262 | return of(true); 263 | } 264 | return this.customValidator(file).pipe( 265 | tap(res => { 266 | if (!res) { 267 | this.validationError.next({ 268 | file, 269 | error: FileValidationTypes.customValidator 270 | }); 271 | } 272 | }) 273 | ); 274 | } 275 | 276 | /** Handles input and drag&drop files */ 277 | handleInputFile(file: File, index): void { 278 | const type = GET_FILE_CATEGORY_TYPE(file.type); 279 | if (type === 'image' && this.enableCropper) { 280 | this.filesForCropper.push(file); 281 | if (!this.currentCropperFile) { 282 | this.openCropper(file); 283 | } 284 | } else { 285 | /** Size is not initially checked on handleInputFiles because of cropper size change */ 286 | if (this.isValidSize(file, file.size)) { 287 | this.pushFile(file); 288 | } 289 | } 290 | } 291 | 292 | /** Validates if upload type is single so another file cannot be added */ 293 | private isValidUploadType(file): boolean { 294 | const isValid = this.fileValidatorService.isValidUploadType(this.files, this.uploadType); 295 | 296 | if (!isValid) { 297 | this.validationError.next({ 298 | file, 299 | error: FileValidationTypes.uploadType 300 | }); 301 | return false; 302 | }; 303 | 304 | return true; 305 | } 306 | 307 | /** Validates max file count */ 308 | private isValidMaxFileCount(files: File[]): boolean { 309 | const isValid = this.fileValidatorService.isValidMaxFileCount(this.fileMaxCount, files, this.files); 310 | 311 | if (isValid) { 312 | return true; 313 | } else { 314 | this.validationError.next({ 315 | file: null, 316 | error: FileValidationTypes.fileMaxCount 317 | }); 318 | return false; 319 | } 320 | } 321 | 322 | /** Add file to file list after succesfull validation */ 323 | pushFile(file: File, fileName = file.name): void { 324 | const newFile = { file, fileName }; 325 | const files = [...this.files, newFile]; 326 | this.setFiles(files); 327 | this.fileAdded.next({ file, fileName }); 328 | this.changeRef.detectChanges(); 329 | } 330 | 331 | /** @description Set files for uploader */ 332 | public setFiles(files: FilePreviewModel[]): void { 333 | this.files = files; 334 | this.changeRef.detectChanges(); 335 | } 336 | 337 | /** Opens cropper for image crop */ 338 | openCropper(file: File): void { 339 | if ((window as any).CROPPER || typeof Cropper !== 'undefined') { 340 | this.safeCropImgUrl = this.fileService.createSafeUrl(file); 341 | this.currentCropperFile = file; 342 | this.changeRef.detectChanges(); 343 | } else { 344 | console.warn("please import cropperjs script and styles to use cropper feature or disable it by setting [enableCropper]='false'" ); 345 | return; 346 | } 347 | } 348 | 349 | /** On img load event */ 350 | cropperImgLoaded(e): void { 351 | const image = document.getElementById('cropper-img'); 352 | this.cropper = new Cropper(image, this.cropperOptions); 353 | } 354 | 355 | /** Close or cancel cropper */ 356 | closeCropper(filePreview: FilePreviewModel): void { 357 | this.currentCropperFile = undefined; 358 | this.cropper = undefined; 359 | this.changeRef.detectChanges(); 360 | setTimeout(() => this._cropClosed$.next(filePreview), 200); 361 | } 362 | 363 | /** Removes files from files list */ 364 | removeFileFromList(file: FilePreviewModel): void { 365 | const files = this.files.filter(f => f.fileName !== file.fileName); 366 | this.setFiles(files); 367 | this.fileRemoved.next(file); 368 | this.changeRef.detectChanges(); 369 | } 370 | 371 | /** Validates file extension */ 372 | private isValidExtension(file: File, fileName: string): boolean { 373 | const isValid = this.fileValidatorService.isValidExtension(fileName, this.fileExtensions); 374 | if (!isValid) { 375 | this.validationError.next({file, error: FileValidationTypes.extensions}); 376 | return false; 377 | } 378 | return true; 379 | } 380 | 381 | /** Validates selected file size and total file size */ 382 | private isValidSize(newFile: File, newFileSize: number): boolean { 383 | /** Validating selected file size */ 384 | const isValidFileSize: boolean = this.fileValidatorService.isValidFileSize(newFileSize, this.fileMaxSize); 385 | const isValidTotalFileSize: boolean = this.fileValidatorService.isValidTotalFileSize(newFile, this.files, this.totalMaxSize); 386 | 387 | if (!isValidFileSize) { 388 | this.validationError.next({ 389 | file: newFile, 390 | error: FileValidationTypes.fileMaxSize 391 | }); 392 | } 393 | 394 | /** Validating Total Files Size */ 395 | if (!isValidTotalFileSize) { 396 | this.validationError.next({ 397 | file: newFile, 398 | error: FileValidationTypes.totalMaxSize 399 | }); 400 | }; 401 | 402 | return isValidFileSize && isValidTotalFileSize; 403 | } 404 | 405 | /** when crop button submitted */ 406 | public onCropSubmit(): void { 407 | const mimeType: string | void = lookup(this.currentCropperFile.name); 408 | if (!mimeType) { 409 | throw new Error("mimeType not found"); 410 | } 411 | this.isCroppingBusy = true; 412 | this.cropper 413 | .getCroppedCanvas(this.croppedCanvasOptions) 414 | .toBlob(this._blobFallBack.bind(this), mimeType); 415 | } 416 | 417 | /** After crop submit */ 418 | private _blobFallBack(blob: Blob): void { 419 | if (!blob) { 420 | return; 421 | } 422 | if (this.isValidSize(blob as File, blob.size)) { 423 | this.pushFile(blob as File, this.currentCropperFile.name); 424 | } 425 | this.closeCropper({ 426 | file: blob as File, 427 | fileName: this.currentCropperFile.name 428 | }); 429 | this.isCroppingBusy = false; 430 | this.changeRef.detectChanges(); 431 | } 432 | 433 | } 434 | --------------------------------------------------------------------------------