UNPKG

ngx-file-drag-drop

Version:

angular material file input component supports file drag and drop, and selection with native file picker

486 lines (479 loc) 19.3 kB
import * as i0 from '@angular/core'; import { Pipe, EventEmitter, forwardRef, Component, HostBinding, Input, Output, ViewChild, HostListener } from '@angular/core'; import * as i1 from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips'; import * as i2 from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import * as i3 from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; class BytePipe { transform(value, decimals = 2) { value = value.toString(); if (parseInt(value, 10) >= 0) { value = this.formatBytes(+value, +decimals); } return value; } // https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript formatBytes(bytes, decimals = 2) { if (bytes === 0) { return "0 Bytes"; } const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: BytePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: BytePipe, isStandalone: true, name: "byte" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: BytePipe, decorators: [{ type: Pipe, args: [{ name: "byte", standalone: true, }] }] }); class NgxFileDragDropComponent { constructor() { this.valueChanged = new EventEmitter(); // does no validation, just sets the hidden file input this.accept = "*"; this._disabled = false; this._multiple = false; this.emptyPlaceholder = `Drop file${this.multiple ? "s" : ""} or click to select`; this._displayFileSize = false; this._activeBorderColor = "purple"; this._files = []; this._isDragOver = false; // https://angular.io/api/forms/ControlValueAccessor this._onChange = (_) => { }; this._onTouched = () => { }; } get disabled() { return this._disabled; } set disabled(val) { this._disabled = coerceBooleanProperty(val); } set multiple(value) { this._multiple = coerceBooleanProperty(value); } get multiple() { return this._multiple; } set displayFileSize(value) { this._displayFileSize = coerceBooleanProperty(value); } get displayFileSize() { return this._displayFileSize; } set activeBorderColor(color) { this._activeBorderColor = color; } get activeBorderColor() { return this.isDragover ? this._activeBorderColor : "#ccc"; } get files() { return this._files; } get isEmpty() { return !this.files?.length; } // @HostBinding('class.drag-over') get isDragover() { return this._isDragOver; } set isDragover(value) { if (!this.disabled) { this._isDragOver = value; } } writeValue(files) { const fileArray = this.convertToArray(files); if (fileArray.length < 2 || this.multiple) { this._files = fileArray; this.emitChanges(this._files); } else { throw Error("Multiple files not allowed"); } } registerOnChange(fn) { this._onChange = fn; } registerOnTouched(fn) { this._onTouched = fn; } setDisabledState(isDisabled) { this.disabled = isDisabled; } emitChanges(files) { this.valueChanged.emit(files); this._onChange(files); } addFiles(files) { // this._onTouched(); const fileArray = this.convertToArray(files); if (this.multiple) { // this.errorOnEqualFilenames(fileArray); const merged = this.files.concat(fileArray); this.writeValue(merged); } else { this.writeValue(fileArray); } } removeFile(file) { const fileIndex = this.files.indexOf(file); if (fileIndex >= 0) { const currentFiles = this.files.slice(); currentFiles.splice(fileIndex, 1); this.writeValue(currentFiles); } } clear() { this.writeValue([]); } change(event) { event.stopPropagation(); this._onTouched(); const fileList = event.target.files; if (fileList?.length) { this.addFiles(fileList); } // clear it so change is triggered if same file is selected again event.target.value = ""; } activate(e) { e.preventDefault(); this.isDragover = true; } deactivate(e) { e.preventDefault(); this.isDragover = false; } handleDrop(e) { this.deactivate(e); if (!this.disabled) { const fileList = e.dataTransfer.files; this.removeDirectories(fileList).then((files) => { if (files?.length) { this.addFiles(files); } this._onTouched(); }); } } open() { if (!this.disabled) { this.fileInputEl?.nativeElement.click(); } } removeDirectories(files) { return new Promise((resolve) => { const fileArray = this.convertToArray(files); const dirnames = []; const readerList = []; for (let i = 0; i < fileArray.length; i++) { const reader = new FileReader(); reader.onerror = () => { dirnames.push(fileArray[i].name); }; reader.onloadend = () => addToReaderList(i); reader.readAsArrayBuffer(fileArray[i]); } function addToReaderList(val) { readerList.push(val); if (readerList.length === fileArray.length) { resolve(fileArray.filter((file) => !dirnames.includes(file.name))); } } }); } convertToArray(files) { if (files) { if (files instanceof File) { return [files]; } else if (Array.isArray(files)) { return files; } else { return Array.prototype.slice.call(files); } } return []; } getFileName(file) { if (!this._displayFileSize) { return file.name; } const size = new BytePipe().transform(file.size); return `${file.name} (${size})`; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: NgxFileDragDropComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.5", type: NgxFileDragDropComponent, isStandalone: true, selector: "ngx-file-drag-drop", inputs: { disabled: "disabled", multiple: "multiple", displayFileSize: "displayFileSize", activeBorderColor: "activeBorderColor", accept: "accept", emptyPlaceholder: "emptyPlaceholder" }, outputs: { valueChanged: "valueChanged" }, host: { listeners: { "change": "change($event)", "dragenter": "activate($event)", "dragover": "activate($event)", "dragleave": "deactivate($event)", "drop": "handleDrop($event)", "click": "open()" }, properties: { "class.disabled": "this.disabled", "style.border-color": "this.activeBorderColor", "class.empty-input": "this.isEmpty" } }, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NgxFileDragDropComponent), multi: true, }, ], viewQueries: [{ propertyName: "fileInputEl", first: true, predicate: ["fileInputEl"], descendants: true }], ngImport: i0, template: ` @if(files.length){ <mat-chip-listbox selectable="false"> @for (file of files; track file) { <mat-chip matTooltip="{{ file.size | byte }}" matTooltipPosition="below" [matTooltipDisabled]="displayFileSize" selected highlighted [disabled]="disabled" color="accent" disableRipple="true" [removable]="!disabled" (removed)="removeFile(file)" > <span class="filename">{{ getFileName(file) }}</span> @if(!disabled){ <mat-icon matChipRemove>cancel</mat-icon> } </mat-chip> } </mat-chip-listbox> } @if (!files.length){ <span class="placeholder">{{ emptyPlaceholder }}</span> } <input #fileInputEl class="hidden" #fileInput type="file" [attr.multiple]="multiple ? '' : null" [attr.accept]="accept" /> `, isInline: true, styles: ["input{width:0px;height:0px;opacity:0;overflow:hidden;position:absolute;z-index:-1}:host{display:block;border:2px dashed;border-radius:20px;min-height:50px;margin:10px auto;max-width:500px;padding:20px;cursor:pointer}:host.disabled{opacity:.5;cursor:unset}.placeholder{color:gray;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}mat-chip{max-width:100%}.filename{max-width:calc(100% - 1em);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}:host.empty-input{display:flex;align-items:center;justify-content:center}.mat-mdc-chip.mat-mdc-standard-chip.mat-focus-indicator{box-shadow:none}.mat-mdc-chip.mat-mdc-standard-chip:after{background:unset}\n"], dependencies: [{ kind: "ngmodule", type: MatChipsModule }, { kind: "component", type: i1.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "component", type: i1.MatChipListbox, selector: "mat-chip-listbox", inputs: ["multiple", "aria-orientation", "selectable", "compareWith", "required", "hideSingleSelectionIndicator", "value"], outputs: ["change"] }, { kind: "directive", type: i1.MatChipRemove, selector: "[matChipRemove]" }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: BytePipe, name: "byte" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: NgxFileDragDropComponent, decorators: [{ type: Component, args: [{ selector: "ngx-file-drag-drop", standalone: true, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NgxFileDragDropComponent), multi: true, }, ], imports: [MatChipsModule, MatTooltipModule, MatIconModule, BytePipe], template: ` @if(files.length){ <mat-chip-listbox selectable="false"> @for (file of files; track file) { <mat-chip matTooltip="{{ file.size | byte }}" matTooltipPosition="below" [matTooltipDisabled]="displayFileSize" selected highlighted [disabled]="disabled" color="accent" disableRipple="true" [removable]="!disabled" (removed)="removeFile(file)" > <span class="filename">{{ getFileName(file) }}</span> @if(!disabled){ <mat-icon matChipRemove>cancel</mat-icon> } </mat-chip> } </mat-chip-listbox> } @if (!files.length){ <span class="placeholder">{{ emptyPlaceholder }}</span> } <input #fileInputEl class="hidden" #fileInput type="file" [attr.multiple]="multiple ? '' : null" [attr.accept]="accept" /> `, styles: ["input{width:0px;height:0px;opacity:0;overflow:hidden;position:absolute;z-index:-1}:host{display:block;border:2px dashed;border-radius:20px;min-height:50px;margin:10px auto;max-width:500px;padding:20px;cursor:pointer}:host.disabled{opacity:.5;cursor:unset}.placeholder{color:gray;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}mat-chip{max-width:100%}.filename{max-width:calc(100% - 1em);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}:host.empty-input{display:flex;align-items:center;justify-content:center}.mat-mdc-chip.mat-mdc-standard-chip.mat-focus-indicator{box-shadow:none}.mat-mdc-chip.mat-mdc-standard-chip:after{background:unset}\n"] }] }], propDecorators: { disabled: [{ type: HostBinding, args: ["class.disabled"] }, { type: Input }], multiple: [{ type: Input }], displayFileSize: [{ type: Input }], activeBorderColor: [{ type: Input }, { type: HostBinding, args: ["style.border-color"] }], isEmpty: [{ type: HostBinding, args: ["class.empty-input"] }], valueChanged: [{ type: Output }], fileInputEl: [{ type: ViewChild, args: ["fileInputEl"] }], accept: [{ type: Input }], emptyPlaceholder: [{ type: Input }], change: [{ type: HostListener, args: ["change", ["$event"]] }], activate: [{ type: HostListener, args: ["dragenter", ["$event"]] }, { type: HostListener, args: ["dragover", ["$event"]] }], deactivate: [{ type: HostListener, args: ["dragleave", ["$event"]] }], handleDrop: [{ type: HostListener, args: ["drop", ["$event"]] }], open: [{ type: HostListener, args: ["click"] }] } }); class FileValidators { static fileExtension(ext) { return (control) => { const validExtensions = ext.map((e) => e.trim().toLowerCase()); const fileArray = control.value; const invalidFiles = fileArray .map((file) => file.name) .filter((fname) => { const extension = fname .slice(((fname.lastIndexOf(".") - 1) >>> 0) + 2) .toLowerCase(); return !validExtensions.includes(extension); }) .map((name) => ({ name, ext: name.slice(((name.lastIndexOf(".") - 1) >>> 0) + 2), })); return !invalidFiles.length ? null : { fileExtension: { requiredExtension: ext.toString(), actualExtensions: invalidFiles, }, }; }; } static uniqueFileNames(control) { const fileNameArray = control.value.map((file) => file.name); const duplicates = fileNameArray.reduce((acc, curr) => { acc[curr] = acc[curr] ? acc[curr] + 1 : 1; return acc; }, {}); const duplicatesArray = Object.entries(duplicates) .filter((arr) => arr[1] > 1) .map((arr) => ({ name: arr[0], count: arr[1] })); return !duplicatesArray.length ? null : { uniqueFileNames: { duplicatedFileNames: duplicatesArray }, }; } static fileType(types) { return (control) => { let regExp; if (Array.isArray(types)) { const joinedTypes = types.join("$|^"); regExp = new RegExp(`^${joinedTypes}$`, "i"); } else { regExp = types; } const fileArray = control.value; const invalidFiles = fileArray .filter((file) => !regExp.test(file.type)) .map((file) => ({ name: file.name, type: file.type })); return !invalidFiles.length ? null : { fileType: { requiredType: types.toString(), actualTypes: invalidFiles, }, }; }; } static maxFileCount(count) { return (control) => { const fileCount = control?.value ? control.value.length : 0; const result = count >= fileCount; return result ? null : { maxFileCount: { maxCount: count, actualCount: fileCount, }, }; }; } static maxFileSize(bytes) { return (control) => { const fileArray = control.value; const invalidFiles = fileArray .filter((file) => file.size > bytes) .map((file) => ({ name: file.name, size: file.size })); return !invalidFiles.length ? null : { maxFileSize: { maxSize: bytes, actualSizes: invalidFiles, }, }; }; } static maxTotalSize(bytes) { return (control) => { const size = control?.value ? control.value .map((file) => file.size) .reduce((acc, i) => acc + i, 0) : 0; const result = bytes >= size; return result ? null : { maxTotalSize: { maxSize: bytes, actualSize: size, }, }; }; } static required(control) { const count = control?.value?.length; return count ? null : { required: true, }; } } /* * Public API Surface of ngx-file-drag-drop */ /** * Generated bundle index. Do not edit. */ export { BytePipe, FileValidators, NgxFileDragDropComponent }; //# sourceMappingURL=ngx-file-drag-drop.mjs.map