fg-file-grab
Version:
Lightweight Angular file dropzone component with icons for PDF, XLS/XLSX/CSV and images; drag & drop, click-to-select, and removal.
175 lines (169 loc) • 10.7 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, Component, Input, Output } from '@angular/core';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
const DEFAULT_ACCEPT = [
'image/jpg', 'image/jpeg', 'image/png',
'application/pdf',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
'.xls', '.xlsx', '.csv', '.pdf', '.jpg', '.jpeg', '.png'
].join(',');
class FileGrabComponent {
constructor() {
this.multiple = false;
this.fileSelected = new EventEmitter();
this.filesChanged = new EventEmitter();
this.defaultAccept = DEFAULT_ACCEPT;
this.dragOver = false;
this.items = [];
}
addFiles(files) {
const arr = Array.from(files);
const added = [];
for (const f of arr) {
// Validate accept if provided
if (this.accept && !this.isAccepted(f))
continue;
// prevent duplicates by name + size
if (this.items.some(x => x.file.name === f.name && x.file.size === f.size))
continue;
const url = URL.createObjectURL(f);
this.items.push({ file: f, url });
added.push(f);
this.fileSelected.emit(f);
}
if (added.length)
this.filesChanged.emit(this.items.map(i => i.file));
}
onFileInput(e) {
const input = e.target;
if (!input.files)
return;
this.addFiles(input.files);
input.value = '';
}
onDragOver(e) {
e.preventDefault();
this.dragOver = true;
}
onDragLeave(_) {
this.dragOver = false;
}
onDrop(e) {
e.preventDefault();
this.dragOver = false;
if (e.dataTransfer?.files?.length) {
this.addFiles(e.dataTransfer.files);
}
}
remove(index) {
const [removed] = this.items.splice(index, 1);
if (removed)
URL.revokeObjectURL(removed.url);
this.filesChanged.emit(this.items.map(i => i.file));
}
ext(file) {
const n = file.name.toLowerCase();
const dot = n.lastIndexOf('.');
return dot >= 0 ? n.substring(dot + 1) : (file.type.split('/')[1] || '').toLowerCase();
}
isAccepted(file) {
const accepts = (this.accept || this.defaultAccept).split(',').map(s => s.trim().toLowerCase());
const name = file.name.toLowerCase();
const mime = file.type.toLowerCase();
return accepts.some(a => {
if (!a)
return false;
if (a.startsWith('.'))
return name.endsWith(a);
return mime === a;
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileGrabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: FileGrabComponent, isStandalone: true, selector: "file-grab", inputs: { uploadHereLabel: "uploadHereLabel", uploadDescLabel: "uploadDescLabel", accept: "accept", multiple: "multiple" }, outputs: { fileSelected: "fileSelected", filesChanged: "filesChanged" }, ngImport: i0, template: `
<div class="fg-container">
<div class="fg-dropzone"
[class.fg-over]="dragOver"
(dragover)="onDragOver($event)"
(dragleave)="onDragLeave($event)"
(drop)="onDrop($event)"
(click)="fileInput.click()">
<input #fileInput type="file" class="fg-hidden" [accept]="accept || defaultAccept" [multiple]="multiple"
(change)="onFileInput($event)" />
<div class="fg-dropzone-text">
<strong>{{ uploadHereLabel || 'Drop your file or click here' }}</strong>
<div class="fg-desc" *ngIf="uploadDescLabel">{{ uploadDescLabel }}</div>
</div>
</div>
<div class="fg-list" *ngIf="items.length">
<div class="fg-item" *ngFor="let it of items; let i = index">
<a class="fg-icon-wrap" [href]="it.url" target="_blank" rel="noopener noreferrer">
<ng-container [ngSwitch]="ext(it.file)">
<div *ngSwitchCase="'pdf'" class="fg-icon fg-icon-pdf">PDF</div>
<div *ngSwitchCase="'xls'" class="fg-icon fg-icon-xls"></div>
<div *ngSwitchCase="'xlsx'" class="fg-icon fg-icon-xls"></div>
<div *ngSwitchCase="'csv'" class="fg-icon fg-icon-xls"></div>
<div *ngSwitchDefault class="fg-icon fg-icon-generic"></div>
</ng-container>
</a>
<div class="fg-name" [title]="it.file.name">{{ it.file.name }}</div>
<button type="button" class="fg-remove" (click)="remove(i)" aria-label="Remove">×</button>
</div>
</div>
</div>
`, isInline: true, styles: [":host{display:block}.fg-container{width:100%}.fg-dropzone{border:2px dashed #CBD5E1;border-radius:6px;padding:24px;text-align:center;color:#0f172a;background:#f8fafc;cursor:pointer;transition:border-color .15s ease,background .15s ease}.fg-over{border-color:#10b981;background:#ecfdf5}.fg-dropzone-text strong{font-weight:600}.fg-desc{color:#64748b;font-size:12px;margin-top:4px}.fg-hidden{display:none}.fg-list{display:flex;gap:24px;margin-top:12px;flex-wrap:wrap}.fg-item{display:flex;flex-direction:column;align-items:center;position:relative}.fg-name{margin-top:6px;max-width:140px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;font-size:12px}.fg-remove{position:absolute;top:-6px;right:-6px;background:#ef4444;color:#fff;border:none;border-radius:999px;width:20px;height:20px;line-height:18px;cursor:pointer}.fg-icon{width:56px;height:72px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-weight:700;color:#fff}.fg-icon-pdf{background:#ef4444}.fg-icon-xls{background:#16a34a;position:relative}.fg-icon-xls:before{content:\"\";width:28px;height:28px;background:repeating-linear-gradient(90deg,#fff 0 6px,transparent 6px 8px),repeating-linear-gradient(0deg,#fff 0 6px,transparent 6px 8px);border-radius:4px}.fg-icon-generic{background:#64748b;position:relative}.fg-icon-generic:before{content:\"\";width:28px;height:36px;background:#fff;border-radius:3px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileGrabComponent, decorators: [{
type: Component,
args: [{ selector: 'file-grab', standalone: true, imports: [CommonModule], template: `
<div class="fg-container">
<div class="fg-dropzone"
[class.fg-over]="dragOver"
(dragover)="onDragOver($event)"
(dragleave)="onDragLeave($event)"
(drop)="onDrop($event)"
(click)="fileInput.click()">
<input #fileInput type="file" class="fg-hidden" [accept]="accept || defaultAccept" [multiple]="multiple"
(change)="onFileInput($event)" />
<div class="fg-dropzone-text">
<strong>{{ uploadHereLabel || 'Drop your file or click here' }}</strong>
<div class="fg-desc" *ngIf="uploadDescLabel">{{ uploadDescLabel }}</div>
</div>
</div>
<div class="fg-list" *ngIf="items.length">
<div class="fg-item" *ngFor="let it of items; let i = index">
<a class="fg-icon-wrap" [href]="it.url" target="_blank" rel="noopener noreferrer">
<ng-container [ngSwitch]="ext(it.file)">
<div *ngSwitchCase="'pdf'" class="fg-icon fg-icon-pdf">PDF</div>
<div *ngSwitchCase="'xls'" class="fg-icon fg-icon-xls"></div>
<div *ngSwitchCase="'xlsx'" class="fg-icon fg-icon-xls"></div>
<div *ngSwitchCase="'csv'" class="fg-icon fg-icon-xls"></div>
<div *ngSwitchDefault class="fg-icon fg-icon-generic"></div>
</ng-container>
</a>
<div class="fg-name" [title]="it.file.name">{{ it.file.name }}</div>
<button type="button" class="fg-remove" (click)="remove(i)" aria-label="Remove">×</button>
</div>
</div>
</div>
`, styles: [":host{display:block}.fg-container{width:100%}.fg-dropzone{border:2px dashed #CBD5E1;border-radius:6px;padding:24px;text-align:center;color:#0f172a;background:#f8fafc;cursor:pointer;transition:border-color .15s ease,background .15s ease}.fg-over{border-color:#10b981;background:#ecfdf5}.fg-dropzone-text strong{font-weight:600}.fg-desc{color:#64748b;font-size:12px;margin-top:4px}.fg-hidden{display:none}.fg-list{display:flex;gap:24px;margin-top:12px;flex-wrap:wrap}.fg-item{display:flex;flex-direction:column;align-items:center;position:relative}.fg-name{margin-top:6px;max-width:140px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;font-size:12px}.fg-remove{position:absolute;top:-6px;right:-6px;background:#ef4444;color:#fff;border:none;border-radius:999px;width:20px;height:20px;line-height:18px;cursor:pointer}.fg-icon{width:56px;height:72px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-weight:700;color:#fff}.fg-icon-pdf{background:#ef4444}.fg-icon-xls{background:#16a34a;position:relative}.fg-icon-xls:before{content:\"\";width:28px;height:28px;background:repeating-linear-gradient(90deg,#fff 0 6px,transparent 6px 8px),repeating-linear-gradient(0deg,#fff 0 6px,transparent 6px 8px);border-radius:4px}.fg-icon-generic{background:#64748b;position:relative}.fg-icon-generic:before{content:\"\";width:28px;height:36px;background:#fff;border-radius:3px}\n"] }]
}], propDecorators: { uploadHereLabel: [{
type: Input
}], uploadDescLabel: [{
type: Input
}], accept: [{
type: Input
}], multiple: [{
type: Input
}], fileSelected: [{
type: Output
}], filesChanged: [{
type: Output
}] } });
/**
* Generated bundle index. Do not edit.
*/
export { FileGrabComponent };
//# sourceMappingURL=fg-file-grab.mjs.map