Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Converted FileDrop into a FileDropDirective. #143

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions src/lib/ngx-drop/file-drop.component.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
<div [className]="dropZoneClassName"
[class.ngx-file-drop__drop-zone--over]="isDraggingOverDropZone"
(drop)="dropFiles($event)"
(dragover)="onDragOver($event)"
(dragleave)="onDragLeave($event)">
<div [className]="dropZoneClassName" fdFileDrop (fdOnFileOver)="onFileOver.emit($event)"
(fdOnFileLeave)="onFileLeave.emit($event)" (fdOnFileDrop)="onFileDrop.emit($event)"
[fdDropZoneClassName]="dropZoneFileOverClassName" [fdAccept]="accept" [fdDisabled]="disabled">
<div [className]="contentClassName">
<ng-content></ng-content>
<div *ngIf="dropZoneLabel" class="ngx-file-drop__drop-zone-label">{{dropZoneLabel}}</div>
<input type="file" #fileSelector [accept]="accept" (change)="uploadFiles($event)" [multiple]="multiple" class="ngx-file-drop__file-input" />
<div *ngIf="showBrowseBtn">
<input type="button" [className]="browseBtnClassName" value="{{browseBtnLabel}}" (click)="onBrowseButtonClick($event)" />
</div>
<file-drop-selector [accept]="accept" [multiple]="multiple" [showBrowseBtn]="showBrowseBtn"
[browseBtnClassName]="browseBtnClassName" [browseBtnLabel]="browseBtnLabel"></file-drop-selector>
</div>
</div>
</div>
288 changes: 12 additions & 276 deletions src/lib/ngx-drop/file-drop.component.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
import {
Component,
Input,
Output,
EventEmitter,
NgZone,
OnDestroy,
Renderer2,
ViewChild,
ElementRef
} from '@angular/core';
import { timer, Subscription } from 'rxjs';
import { Component, Input, Output, EventEmitter } from '@angular/core';

import { UploadFile } from './upload-file.model';
import { UploadEvent } from './upload-event.model';
import { FileSystemFileEntry, FileSystemEntry, FileSystemDirectoryEntry } from './dom.types';

@Component({
selector: 'file-drop',
templateUrl: './file-drop.component.html',
styleUrls: ['./file-drop.component.scss'],
styleUrls: ['./file-drop.component.scss']
})
export class FileComponent implements OnDestroy {

export class FileComponent {
@Input()
public accept: string = '*';

@@ -34,280 +20,30 @@ export class FileComponent implements OnDestroy {
@Input()
public dropZoneClassName: string = 'ngx-file-drop__drop-zone';

@Input()
public dropZoneFileOverClassName: string = 'ngx-file-drop__drop-zone--over';

@Input()
public contentClassName: string = 'ngx-file-drop__content';

public get disabled(): boolean { return this._disabled; }
@Input()
public set disabled(value: boolean) {
this._disabled = (value != null && `${value}` !== 'false');
}
public disabled = false;

@Input()
public showBrowseBtn: boolean = false;
@Input()
public browseBtnClassName: string = 'btn btn-primary btn-xs ngx-file-drop__browse-btn';
public browseBtnClassName: string =
'btn btn-primary btn-xs ngx-file-drop__browse-btn';

@Input()
public browseBtnLabel: string = 'Browse files';

@Output()
public onFileDrop: EventEmitter<UploadEvent> = new EventEmitter<UploadEvent>();
public onFileDrop: EventEmitter<UploadEvent> = new EventEmitter<
UploadEvent
>();
@Output()
public onFileOver: EventEmitter<any> = new EventEmitter<any>();
@Output()
public onFileLeave: EventEmitter<any> = new EventEmitter<any>();

@ViewChild('fileSelector')
public fileSelector: ElementRef;

public isDraggingOverDropZone: boolean = false;

private globalDraggingInProgress: boolean = false;
private globalDragStartListener: () => void;
private globalDragEndListener: () => void;

private files: UploadFile[] = [];
private numOfActiveReadEntries: number = 0;

private helperFormEl: HTMLFormElement | null = null;
private fileInputPlaceholderEl: HTMLDivElement | null = null;

private dropEventTimerSubscription: Subscription | null = null;

private _disabled: boolean = false;

constructor(
private zone: NgZone,
private renderer: Renderer2
) {
this.globalDragStartListener = this.renderer.listen('document', 'dragstart', (evt: Event) => {
this.globalDraggingInProgress = true;
});
this.globalDragEndListener = this.renderer.listen('document', 'dragend', (evt: Event) => {
this.globalDraggingInProgress = false;
});
}

public ngOnDestroy(): void {
if (this.dropEventTimerSubscription) {
this.dropEventTimerSubscription.unsubscribe();
this.dropEventTimerSubscription = null;
}
this.globalDragStartListener();
this.globalDragEndListener();
this.files = [];
this.helperFormEl = null;
this.fileInputPlaceholderEl = null;
}

public onDragOver(event: Event): void {
if (!this.isDropzoneDisabled()) {
if (!this.isDraggingOverDropZone) {
this.isDraggingOverDropZone = true;
this.onFileOver.emit(event);
}
this.preventAndStop(event);
}
}

public onDragLeave(event: Event): void {
if (!this.isDropzoneDisabled()) {
if (this.isDraggingOverDropZone) {
this.isDraggingOverDropZone = false;
this.onFileLeave.emit(event);
}
this.preventAndStop(event);
}
}

public dropFiles(event: DragEvent): void {
if (!this.isDropzoneDisabled()) {
this.isDraggingOverDropZone = false;
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy';
let items: FileList | DataTransferItemList;
if (event.dataTransfer.items) {
items = event.dataTransfer.items;
} else {
items = event.dataTransfer.files;
}
this.preventAndStop(event);
this.checkFiles(items);
}
}
}

public onBrowseButtonClick(event?: MouseEvent): void {
if (this.fileSelector && this.fileSelector.nativeElement) {
(this.fileSelector.nativeElement as HTMLInputElement).click();
}
}

/**
* Processes the change event of the file input and adds the given files.
* @param Event event
*/
public uploadFiles(event: Event): void {
if (!this.isDropzoneDisabled()) {
if (event.target) {
const items = (event.target as HTMLInputElement).files || ([] as any);
this.checkFiles(items);
this.resetFileInput();
}
}
}

private checkFiles(items: FileList | DataTransferItemList): void {
for (let i = 0; i < items.length; i++) {
const item = items[i];
let entry: FileSystemEntry | null = null;
if (this.canGetAsEntry(item)) {
entry = item.webkitGetAsEntry();
}

if (!entry) {
if (item) {
const fakeFileEntry: FileSystemFileEntry = {
name: (item as File).name,
isDirectory: false,
isFile: true,
file: (callback: (filea: File) => void): void => {
callback(item as File);
},
};
const toUpload: UploadFile = new UploadFile(fakeFileEntry.name, fakeFileEntry);
this.addToQueue(toUpload);
}

} else {
if (entry.isFile) {
const toUpload: UploadFile = new UploadFile(entry.name, entry);
this.addToQueue(toUpload);

} else if (entry.isDirectory) {
this.traverseFileTree(entry, entry.name);
}
}
}

if (this.dropEventTimerSubscription) {
this.dropEventTimerSubscription.unsubscribe();
}
this.dropEventTimerSubscription = timer(200, 200)
.subscribe(() => {
if (this.files.length > 0 && this.numOfActiveReadEntries === 0) {
this.onFileDrop.emit(new UploadEvent(this.files));
this.files = [];
}
});
}

private traverseFileTree(item: FileSystemEntry, path: string): void {
if (item.isFile) {
const toUpload: UploadFile = new UploadFile(path, item);
this.files.push(toUpload);

} else {
path = path + '/';
const dirReader = (item as FileSystemDirectoryEntry).createReader();
let entries: FileSystemEntry[] = [];

const readEntries = () => {
this.numOfActiveReadEntries++;
dirReader.readEntries((result) => {
if (!result.length) {
// add empty folders
if (entries.length === 0) {
const toUpload: UploadFile = new UploadFile(path, item);
this.zone.run(() => {
this.addToQueue(toUpload);
});

} else {
for (let i = 0; i < entries.length; i++) {
this.zone.run(() => {
this.traverseFileTree(entries[i], path + entries[i].name);
});
}
}

} else {
// continue with the reading
entries = entries.concat(result);
readEntries();
}

this.numOfActiveReadEntries--;
});
};

readEntries();
}
}

/**
* Clears any added files from the file input element so the same file can subsequently be added multiple times.
*/
private resetFileInput(): void {
if (this.fileSelector && this.fileSelector.nativeElement) {
const fileInputEl = this.fileSelector.nativeElement as HTMLInputElement;
const fileInputContainerEl = fileInputEl.parentElement;
const helperFormEl = this.getHelperFormElement();
const fileInputPlaceholderEl = this.getFileInputPlaceholderElement();

// Just a quick check so we do not mess up the DOM (will never happen though).
if (fileInputContainerEl !== helperFormEl) {
// Insert the form input placeholder in the DOM before the form input element.
this.renderer.insertBefore(fileInputContainerEl, fileInputPlaceholderEl, fileInputEl);
// Add the form input as child of the temporary form element, removing the form input from the DOM.
this.renderer.appendChild(helperFormEl, fileInputEl);
// Reset the form, thus clearing the input element of any files.
helperFormEl.reset();
// Add the file input back to the DOM in place of the file input placeholder element.
this.renderer.insertBefore(fileInputContainerEl, fileInputEl, fileInputPlaceholderEl);
// Remove the input placeholder from the DOM
this.renderer.removeChild(fileInputContainerEl, fileInputPlaceholderEl);
}
}
}

/**
* Get a cached HTML form element as a helper element to clear the file input element.
*/
private getHelperFormElement(): HTMLFormElement {
if (!this.helperFormEl) {
this.helperFormEl = this.renderer.createElement('form') as HTMLFormElement;
}

return this.helperFormEl;
}

/**
* Get a cached HTML div element to be used as placeholder for the file input element when clearing said element.
*/
private getFileInputPlaceholderElement(): HTMLDivElement {
if (!this.fileInputPlaceholderEl) {
this.fileInputPlaceholderEl = this.renderer.createElement('div') as HTMLDivElement;
}

return this.fileInputPlaceholderEl;
}

private canGetAsEntry(item: any): item is DataTransferItem {
return !!item.webkitGetAsEntry;
}

private isDropzoneDisabled(): boolean {
return (this.globalDraggingInProgress || this.disabled);
}

private addToQueue(item: UploadFile): void {
this.files.push(item);
}

private preventAndStop(event: Event): void {
event.stopPropagation();
event.preventDefault();
}
}
Loading
Oops, something went wrong.