@avoraui/av-file-picker
Version:
A customizable Angular file picker component for images and PDFs with preview support.
196 lines (191 loc) • 20 kB
JavaScript
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