UNPKG

@avoraui/av-file-picker

Version:

A customizable Angular file picker component for images and PDFs with preview support.

196 lines (191 loc) 20 kB
import * as i0 from '@angular/core'; import { forwardRef, Input, Component } from '@angular/core'; import { ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; import { MatCard } from '@angular/material/card'; import { MatButton } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; class AvFilePicker { defaultImage; showPdfDetails; imageURL; pdfFileName = ""; isPDF = false; viewPdf = false; convertValue = ""; decodeValue; pdfUrl; // Max file size of 10MB MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB constructor() { } onChange = () => { }; onTouched = () => { }; disabled = false; /** * Handles the selection of an image or PDF file from an input event and processes the file accordingly. * It validates the file size, checks its format, and reads the file to produce a base64-encoded string. * Updates the relevant properties depending on whether the file is an image or a PDF. * * @param {any} event - The event triggered by the input file selection. * @return {void} This method does not return a value. */ selectImage(event) { if (event.target.files) { const file = event.target.files[0]; const reader = new FileReader(); // Validate file size if (file.size > this.MAX_FILE_SIZE) { console.error('File is too large (max 10MB).'); alert('File size exceeds the 10MB limit.'); return; } // Determine if the file is a PDF if (file.type === 'application/pdf') { if (this.pdfUrl) { this.viewPdf = false; URL.revokeObjectURL(this.pdfUrl); } this.isPDF = true; this.showPdfDetails = true; this.pdfFileName = file.name; reader.onload = (e) => { const binaryString = e.target?.result; this.convertValue = btoa(binaryString); this.onChange(this.convertValue); }; reader.onerror = (e) => { console.error('Error reading PDF file', e); alert('Error reading the PDF file.'); }; reader.readAsBinaryString(file); } else { // Assume the file is an image reader.onload = (e) => { this.imageURL = e.target.result; this.convertValue = btoa(this.imageURL); this.onChange(this.convertValue); }; reader.onerror = (e) => { console.error('Error reading image file', e); alert('Error reading the image file.'); }; reader.readAsDataURL(file); } } } /** * Resets the image-related properties to their default states. * Sets the imageURL to the default image, marks the object as not a PDF, * disables the PDF viewer, and triggers an update with the encoded imageURL. * * @return {void} Does not return a value. */ clearImage() { this.imageURL = this.defaultImage; this.isPDF = false; this.viewPdf = false; this.onChange(btoa(this.imageURL)); } /** * Processes and decodes a provided Base64-encoded value, determining if it is an image or a PDF. * Updates the relevant properties based on the decoded content or handles errors gracefully. * * @param {any} value - The Base64-encoded string to process. If null, undefined, or an empty string, it defaults to a predefined image. * @return {void} No return value. */ writeValue(value) { if (value === null || value === undefined || value === '') { this.imageURL = this.defaultImage; // Set to default image if no value provided } else { try { // Decode the Base64 value this.decodeValue = atob(value); // Convert the decoded string into a Uint8Array (binary data) const byteCharacters = new Uint8Array(this.decodeValue.length); for (let i = 0; i < this.decodeValue.length; i++) { byteCharacters[i] = this.decodeValue.charCodeAt(i); } // Check for file signature to identify PDF or image const pdfSignature = byteCharacters.slice(0, 4); // First 4 bytes for PDF const isPDF = pdfSignature[0] === 0x25 && pdfSignature[1] === 0x50 && pdfSignature[2] === 0x44 && pdfSignature[3] === 0x46; if (isPDF) { // It's a PDF, create a Blob and Object URL const blob = new Blob([byteCharacters], { type: 'application/pdf' }); this.pdfUrl = URL.createObjectURL(blob); this.viewPdf = true; this.showPdfDetails = true; this.pdfFileName = ""; } else { this.isPDF = false; this.showPdfDetails = false; this.imageURL = this.decodeValue; } } catch (error) { console.error('Error decoding Base64 value or handling file:', error); this.imageURL = this.defaultImage; // Reset to default image on error } } } registerOnChange(fn) { this.onChange = fn; } registerOnTouched(fn) { this.onTouched = fn; } setDisabledState(isDisabled) { this.disabled = isDisabled; } /** * Performs cleanup and resource deallocation before the component is destroyed. * Specifically, it revokes any created Object URLs to prevent memory leaks. * * @return {void} Does not return a value. */ ngOnDestroy() { // Clean up Object URL to prevent memory leaks if (this.pdfUrl) { URL.revokeObjectURL(this.pdfUrl); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: AvFilePicker, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.2", type: AvFilePicker, isStandalone: true, selector: "av-file-picker", inputs: { defaultImage: "defaultImage", showPdfDetails: "showPdfDetails" }, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AvFilePicker), multi: true } ], ngImport: i0, template: "<mat-card class=\"img-upload-panel\">\r\n <div class=\"img-preview-container\">\r\n <div class=\"img-placeholder\" [class.has-image]=\"imageURL || isPDF\" [class.has-pdf]=\"isPDF\">\r\n\r\n <!-- Regular Image -->\r\n @if (imageURL && !isPDF) {\r\n <img [src]=\"imageURL\" class=\"preview-image\" alt=\"Selected file\">\r\n }\r\n\r\n <!-- PDF Icon Display -->\r\n @if (isPDF) {\r\n <div class=\"pdf-placeholder\">\r\n <mat-icon class=\"pdf-icon\">picture_as_pdf</mat-icon>\r\n <span class=\"pdf-text\">PDF Document</span>\r\n </div>\r\n }\r\n\r\n <!-- Empty State -->\r\n @if (!imageURL && !isPDF) {\r\n <div class=\"placeholder-content\">\r\n <mat-icon class=\"placeholder-icon\">cloud_upload</mat-icon>\r\n <span class=\"placeholder-text\">No file selected</span>\r\n </div>\r\n }\r\n\r\n <!-- PDF View Link Overlay -->\r\n @if (viewPdf && showPdfDetails && isPDF) {\r\n <div class=\"pdf-overlay\">\r\n <a [href]=\"pdfUrl\" target=\"_blank\" class=\"pdf-link\">\r\n <mat-icon>visibility</mat-icon>\r\n <span>View PDF</span>\r\n </a>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Hidden File Input -->\r\n <input #fileInput\r\n type=\"file\"\r\n class=\"file-input\"\r\n (change)=\"selectImage($event)\"\r\n accept=\"image/*,.pdf\">\r\n </div>\r\n\r\n <!-- File Name Display -->\r\n @if (isPDF && showPdfDetails) {\r\n <div class=\"file-info\">\r\n <mat-icon class=\"file-icon\">description</mat-icon>\r\n <span class=\"file-name\">{{ pdfFileName }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Action Buttons -->\r\n <div class=\"action-buttons\">\r\n <button mat-raised-button\r\n color=\"primary\"\r\n class=\"select-btn\"\r\n (click)=\"fileInput.click()\">\r\n <mat-icon>folder_open</mat-icon>\r\n Select\r\n </button>\r\n\r\n <button mat-raised-button\r\n color=\"warn\"\r\n class=\"clear-btn\"\r\n (click)=\"clearImage()\"\r\n [disabled]=\"!imageURL && !isPDF\">\r\n <mat-icon>clear</mat-icon>\r\n Clear\r\n </button>\r\n </div>\r\n</mat-card>\r\n", styles: [".img-upload-panel{width:280px;padding:24px;border-radius:12px;background:#fff;box-shadow:0 4px 20px #0000000f;border:1px solid #e5e7eb;transition:all .3s cubic-bezier(.4,0,.2,1)}.img-upload-panel:hover{box-shadow:0 8px 32px #00000014;transform:translateY(-2px)}.img-preview-container{position:relative;margin-bottom:20px}.img-placeholder{width:100%;height:200px;border-radius:8px;border:2px dashed #d1d5db;background:#f8fafc;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;transition:all .3s ease}.img-placeholder:hover{border-color:#94a3b8;background:#f1f5f9}.img-placeholder.has-image,.img-placeholder.has-pdf{border:1px solid #e2e8f0;background:#fff}.img-placeholder.has-pdf{background:#fafbfc}.preview-image{width:100%;height:100%;object-fit:cover;border-radius:6px}.placeholder-content{display:flex;flex-direction:column;align-items:center;color:#94a3b8}.placeholder-content .placeholder-icon{font-size:48px;width:48px;height:48px;margin-bottom:8px;opacity:.7}.placeholder-content .placeholder-text{font-size:14px;font-weight:500}.pdf-placeholder{display:flex;flex-direction:column;align-items:center;color:#64748b}.pdf-placeholder .pdf-icon{font-size:64px;width:64px;height:64px;margin-bottom:12px;color:#dc2626;opacity:.8}.pdf-placeholder .pdf-text{font-size:14px;font-weight:600;color:#475569}.pdf-overlay{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#fffffff5;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:8px;padding:12px 16px;box-shadow:0 4px 12px #00000014;border:1px solid #e2e8f0;animation:pulse 2s infinite}.pdf-overlay .pdf-link{display:flex;align-items:center;gap:8px;color:#475569;text-decoration:none;font-weight:600;font-size:14px}.pdf-overlay .pdf-link:hover{color:#334155}.pdf-overlay .pdf-link mat-icon{font-size:20px;width:20px;height:20px}.file-input{display:none}.file-info{display:flex;align-items:center;gap:8px;padding:12px;background:#f1f5f9;border-radius:6px;margin-bottom:20px;border:1px solid #e2e8f0}.file-info .file-icon{color:#475569;font-size:20px;width:20px;height:20px}.file-info .file-name{font-size:14px;color:#475569;font-weight:500;word-break:break-word}.action-buttons{display:flex;gap:12px}.action-buttons button{flex:1;height:44px;font-weight:600;font-size:14px;border-radius:8px;transition:all .3s ease;border:1px solid transparent}.action-buttons button mat-icon{margin-right:8px;font-size:18px;width:18px;height:18px}.action-buttons .select-btn{background:#3b82f6;color:#fff;border-color:#2563eb}.action-buttons .select-btn:hover:not([disabled]){background:#2563eb;border-color:#1d4ed8;transform:translateY(-1px);box-shadow:0 4px 12px #3b82f64d}.action-buttons .select-btn:active{transform:translateY(0)}.action-buttons .clear-btn{background:#ef4444;color:#fff;border-color:#dc2626}.action-buttons .clear-btn:hover:not([disabled]){background:#dc2626;border-color:#b91c1c;transform:translateY(-1px);box-shadow:0 4px 12px #ef44444d}.action-buttons .clear-btn:disabled{background:#f1f5f9;color:#cbd5e1;border-color:#e2e8f0;cursor:not-allowed;transform:none;box-shadow:none}.action-buttons .clear-btn:active:not([disabled]){transform:translateY(0)}@keyframes pulse{0%,to{opacity:1;transform:translate(-50%,-50%) scale(1)}50%{opacity:.9;transform:translate(-50%,-50%) scale(1.02)}}@media (max-width: 768px){.img-upload-panel{width:100%;max-width:320px}}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: AvFilePicker, decorators: [{ type: Component, args: [{ selector: 'av-file-picker', imports: [ ReactiveFormsModule, MatCard, MatButton, MatIcon, ], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AvFilePicker), multi: true } ], standalone: true, template: "<mat-card class=\"img-upload-panel\">\r\n <div class=\"img-preview-container\">\r\n <div class=\"img-placeholder\" [class.has-image]=\"imageURL || isPDF\" [class.has-pdf]=\"isPDF\">\r\n\r\n <!-- Regular Image -->\r\n @if (imageURL && !isPDF) {\r\n <img [src]=\"imageURL\" class=\"preview-image\" alt=\"Selected file\">\r\n }\r\n\r\n <!-- PDF Icon Display -->\r\n @if (isPDF) {\r\n <div class=\"pdf-placeholder\">\r\n <mat-icon class=\"pdf-icon\">picture_as_pdf</mat-icon>\r\n <span class=\"pdf-text\">PDF Document</span>\r\n </div>\r\n }\r\n\r\n <!-- Empty State -->\r\n @if (!imageURL && !isPDF) {\r\n <div class=\"placeholder-content\">\r\n <mat-icon class=\"placeholder-icon\">cloud_upload</mat-icon>\r\n <span class=\"placeholder-text\">No file selected</span>\r\n </div>\r\n }\r\n\r\n <!-- PDF View Link Overlay -->\r\n @if (viewPdf && showPdfDetails && isPDF) {\r\n <div class=\"pdf-overlay\">\r\n <a [href]=\"pdfUrl\" target=\"_blank\" class=\"pdf-link\">\r\n <mat-icon>visibility</mat-icon>\r\n <span>View PDF</span>\r\n </a>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Hidden File Input -->\r\n <input #fileInput\r\n type=\"file\"\r\n class=\"file-input\"\r\n (change)=\"selectImage($event)\"\r\n accept=\"image/*,.pdf\">\r\n </div>\r\n\r\n <!-- File Name Display -->\r\n @if (isPDF && showPdfDetails) {\r\n <div class=\"file-info\">\r\n <mat-icon class=\"file-icon\">description</mat-icon>\r\n <span class=\"file-name\">{{ pdfFileName }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Action Buttons -->\r\n <div class=\"action-buttons\">\r\n <button mat-raised-button\r\n color=\"primary\"\r\n class=\"select-btn\"\r\n (click)=\"fileInput.click()\">\r\n <mat-icon>folder_open</mat-icon>\r\n Select\r\n </button>\r\n\r\n <button mat-raised-button\r\n color=\"warn\"\r\n class=\"clear-btn\"\r\n (click)=\"clearImage()\"\r\n [disabled]=\"!imageURL && !isPDF\">\r\n <mat-icon>clear</mat-icon>\r\n Clear\r\n </button>\r\n </div>\r\n</mat-card>\r\n", styles: [".img-upload-panel{width:280px;padding:24px;border-radius:12px;background:#fff;box-shadow:0 4px 20px #0000000f;border:1px solid #e5e7eb;transition:all .3s cubic-bezier(.4,0,.2,1)}.img-upload-panel:hover{box-shadow:0 8px 32px #00000014;transform:translateY(-2px)}.img-preview-container{position:relative;margin-bottom:20px}.img-placeholder{width:100%;height:200px;border-radius:8px;border:2px dashed #d1d5db;background:#f8fafc;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;transition:all .3s ease}.img-placeholder:hover{border-color:#94a3b8;background:#f1f5f9}.img-placeholder.has-image,.img-placeholder.has-pdf{border:1px solid #e2e8f0;background:#fff}.img-placeholder.has-pdf{background:#fafbfc}.preview-image{width:100%;height:100%;object-fit:cover;border-radius:6px}.placeholder-content{display:flex;flex-direction:column;align-items:center;color:#94a3b8}.placeholder-content .placeholder-icon{font-size:48px;width:48px;height:48px;margin-bottom:8px;opacity:.7}.placeholder-content .placeholder-text{font-size:14px;font-weight:500}.pdf-placeholder{display:flex;flex-direction:column;align-items:center;color:#64748b}.pdf-placeholder .pdf-icon{font-size:64px;width:64px;height:64px;margin-bottom:12px;color:#dc2626;opacity:.8}.pdf-placeholder .pdf-text{font-size:14px;font-weight:600;color:#475569}.pdf-overlay{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#fffffff5;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:8px;padding:12px 16px;box-shadow:0 4px 12px #00000014;border:1px solid #e2e8f0;animation:pulse 2s infinite}.pdf-overlay .pdf-link{display:flex;align-items:center;gap:8px;color:#475569;text-decoration:none;font-weight:600;font-size:14px}.pdf-overlay .pdf-link:hover{color:#334155}.pdf-overlay .pdf-link mat-icon{font-size:20px;width:20px;height:20px}.file-input{display:none}.file-info{display:flex;align-items:center;gap:8px;padding:12px;background:#f1f5f9;border-radius:6px;margin-bottom:20px;border:1px solid #e2e8f0}.file-info .file-icon{color:#475569;font-size:20px;width:20px;height:20px}.file-info .file-name{font-size:14px;color:#475569;font-weight:500;word-break:break-word}.action-buttons{display:flex;gap:12px}.action-buttons button{flex:1;height:44px;font-weight:600;font-size:14px;border-radius:8px;transition:all .3s ease;border:1px solid transparent}.action-buttons button mat-icon{margin-right:8px;font-size:18px;width:18px;height:18px}.action-buttons .select-btn{background:#3b82f6;color:#fff;border-color:#2563eb}.action-buttons .select-btn:hover:not([disabled]){background:#2563eb;border-color:#1d4ed8;transform:translateY(-1px);box-shadow:0 4px 12px #3b82f64d}.action-buttons .select-btn:active{transform:translateY(0)}.action-buttons .clear-btn{background:#ef4444;color:#fff;border-color:#dc2626}.action-buttons .clear-btn:hover:not([disabled]){background:#dc2626;border-color:#b91c1c;transform:translateY(-1px);box-shadow:0 4px 12px #ef44444d}.action-buttons .clear-btn:disabled{background:#f1f5f9;color:#cbd5e1;border-color:#e2e8f0;cursor:not-allowed;transform:none;box-shadow:none}.action-buttons .clear-btn:active:not([disabled]){transform:translateY(0)}@keyframes pulse{0%,to{opacity:1;transform:translate(-50%,-50%) scale(1)}50%{opacity:.9;transform:translate(-50%,-50%) scale(1.02)}}@media (max-width: 768px){.img-upload-panel{width:100%;max-width:320px}}\n"] }] }], ctorParameters: () => [], propDecorators: { defaultImage: [{ type: Input }], showPdfDetails: [{ type: Input }] } }); /* * Public API Surface of av-file-picker */ /** * Generated bundle index. Do not edit. */ export { AvFilePicker }; //# sourceMappingURL=avoraui-av-file-picker.mjs.map