UNPKG

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
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