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
JavaScript
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: `
{
<mat-chip-listbox selectable="false">
(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>
{
<mat-icon matChipRemove>cancel</mat-icon>
}
</mat-chip>
}
</mat-chip-listbox>
} (!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: `
{
<mat-chip-listbox selectable="false">
(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>
{
<mat-icon matChipRemove>cancel</mat-icon>
}
</mat-chip>
}
</mat-chip-listbox>
} (!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