projects/angular/directives/ui-drag-and-drop-file/src/ui-drag-and-drop-file.directive.ts
A directive that facilitates file inputs
and file drop regions
.
OnChanges
AfterViewInit
OnDestroy
Selector | [uiDragAndDropFile] |
Inputs |
Outputs |
HostBindings |
HostListeners |
disabled | |
Type : boolean
|
|
Default value : false
|
|
Configures the |
fileBrowseRef | |
Type : Element
|
|
The element reference that triggers file |
fileClearRef | |
Type : Element
|
|
The element reference that serve as a |
fileType | |
Type : string
|
|
The accepted |
multiple | |
Type : boolean
|
|
Default value : false
|
|
Configures if the |
fileChange | |
Type : EventEmitter
|
|
An event that emits when the file selection state |
fileClear | |
Type : EventEmitter
|
|
An event that emits when the file selection is |
class.file-dragging |
Type : boolean
|
Default value : false
|
dragend | ||||||
Clears the
Parameters :
|
dragenter | ||||||
Arguments : '$event'
|
||||||
Prevents weird flickering
Parameters :
|
dragleave | ||||||
Arguments : '$event'
|
||||||
Clears the
Parameters :
|
dragover | ||||||
Arguments : '$event'
|
||||||
Marks the
Parameters :
|
drop | ||||||
Arguments : '$event'
|
||||||
Reacts to
Parameters :
|
import {
AfterViewInit,
Directive,
ElementRef,
EventEmitter,
HostBinding,
HostListener,
Input,
OnChanges,
OnDestroy,
Output,
Renderer2,
SimpleChanges,
} from '@angular/core';
import { isInternetExplorer } from '@uipath/angular/utilities';
/**
* A directive that facilitates `file inputs` and `file drop regions`.
*
* @export
*/
@Directive({
selector: '[uiDragAndDropFile]',
})
export class UiDragAndDropFileDirective implements OnChanges, AfterViewInit, OnDestroy {
/**
* The accepted `file-type`.
*
*/
@Input()
fileType?: string;
/**
* The element reference that triggers file `browsing`.
*
*/
@Input()
fileBrowseRef?: Element;
/**
* The element reference that serve as a `clear` trigger for the selected files.
*
*/
@Input()
fileClearRef?: Element;
/**
* Configures if the `file area` accepts multiple files.
*
*/
@Input()
multiple = false;
/**
* Configures the `disabled` state of the `file area`.
*
*/
@Input()
disabled = false;
/**
* An event that emits when the file selection state `changes`.
*
*/
@Output()
fileChange = new EventEmitter<File[]>();
/**
* An event that emits when the file selection is `cleared`.
*
*/
@Output()
fileClear = new EventEmitter();
@HostBinding('class.file-dragging')
protected _isDragging = false;
private _disposalCallbacks: (() => void)[] = [];
private _fileInput: HTMLInputElement;
/**
* @ignore
*/
constructor(
private _elementRef: ElementRef,
private _renderer: Renderer2,
) {
this._fileInput = this._renderer.createElement('input') as HTMLInputElement;
this._renderer.setProperty(this._fileInput, 'type', 'file');
this._renderer.setProperty(this._fileInput, 'value', null);
this._renderer.setAttribute(this._fileInput, 'hidden', '');
this._renderer.appendChild(this._elementRef.nativeElement, this._fileInput);
}
/**
* @ignore
*/
ngOnChanges(changes: SimpleChanges) {
if (changes.fileType) {
const fileType = changes.fileType.currentValue;
this._renderer.setProperty(this._fileInput, 'accept', fileType || '');
}
}
/**
* @ignore
*/
ngAfterViewInit() {
if (this.multiple) {
this._renderer.setProperty(this._fileInput, 'multiple', 'true');
}
this._preventEnterOnChildren(this._elementRef.nativeElement);
if (!this.fileBrowseRef) {
this.fileBrowseRef = this._elementRef.nativeElement;
}
this._renderer.setStyle(this.fileBrowseRef, 'cursor', 'pointer');
['click', 'keydown.enter', 'keydown.space'].forEach(eventName => {
const browse = this._renderer
.listen(this.fileBrowseRef, eventName, () => {
if (this.disabled) { return; }
this._fileInput.click();
});
this._disposalCallbacks.push(browse);
});
const change = this._renderer.listen(this._fileInput, 'change', (ev) => {
this._preventAll(ev);
if (this.disabled) { return; }
const target = ev.target as HTMLInputElement;
if (target.files) {
this._emitFiles(target.files);
this._renderer.setProperty(this._fileInput, 'value', null);
}
});
this._disposalCallbacks.push(change);
if (this.fileClearRef) {
const clear = this._renderer
.listen(this.fileClearRef, 'click', () => {
if (this.disabled) { return; }
this._renderer.setProperty(this._fileInput, 'value', null);
this.fileClear.emit();
});
this._disposalCallbacks.push(clear);
}
}
/**
* @ignore
*/
ngOnDestroy() {
this._disposalCallbacks.forEach(dispose => {
dispose();
});
}
/**
* Reacts to `drop` events.
*
* @param ev The `DragEvent` data associated to the `drop`.
*/
@HostListener('drop', ['$event'])
protected _onDrop(ev: DragEvent) {
this._preventAll(ev);
if (this.disabled) { return; }
this._isDragging = false;
this._emitFiles(ev.dataTransfer!.files);
}
/**
* Marks the `state` as `dragging` when a `dragover` event occurs.
*
* @param ev The `DragEvent` data associated to the `dragover`.
*/
@HostListener('dragover', ['$event'])
protected _onDragOver(ev: DragEvent) {
this._preventAll(ev);
if (
this.disabled ||
!this.multiple &&
ev.dataTransfer &&
ev.dataTransfer.items &&
ev.dataTransfer.items.length > 1
) { return; }
this._isDragging = true;
}
/**
* Clears the `dragging` `state` when a `dragleave` event occurs.
*
* @param ev The `DragEvent` data associated to the `dragover`.
*/
@HostListener('dragleave', ['$event'])
protected _onDragLeave(ev: DragEvent) {
this._preventAll(ev);
if (this.disabled) { return; }
this._isDragging = false;
}
/**
* Clears the `dragging` `state` when a `dragleave` event occurs.
*
* @param ev The `DragEvent` data associated to the `dragend`.
*/
@HostListener('dragend')
protected _onDragEnd() {
if (this.disabled) { return; }
this._isDragging = false;
}
/**
* Prevents weird flickering `cross-browser`.
*
* @param ev The `DragEvent` data associated to the `dragenter`.
*/
@HostListener('dragenter', ['$event'])
protected _onDragEnter(ev: DragEvent) {
this._preventAll(ev);
if (this.disabled) { return; }
}
private _emitFiles(files: FileList) {
if (
!files?.length ||
this.disabled
) { return; }
const acceptedExtensions = (this.fileType ?? '')
.split(',')
.map(e => e.trim().toLowerCase());
const isAccepted = (file: File) => this.fileType
? acceptedExtensions.some(extension => file.name.toLowerCase().endsWith(extension))
: true;
const emittedFiles = Array.from(files).filter(isAccepted);
if (
!emittedFiles.length ||
emittedFiles.length > 1 && !this.multiple
) { return; }
this.fileChange.emit(emittedFiles);
}
private _preventEnterOnChildren(element: Element) {
if (!isInternetExplorer()) { return; }
if (element.children.length) {
const children: Element[] = Array.from(element.children);
for (const child of children) {
const dragenter = this._renderer.listen(child, 'dragenter', this._preventAll);
const dragleave = this._renderer.listen(child, 'dragleave', this._preventAll);
this._disposalCallbacks.push(
dragenter,
dragleave,
);
}
}
}
private _preventAll(ev: Event) {
ev.preventDefault();
ev.stopPropagation();
}
}