Refactor dees-input-fileupload component and styles
- Updated demo.ts to enhance layout and styling, including renaming classes and adjusting spacing. - Removed unused template rendering logic from template.ts. - Simplified index.ts by removing the export of renderFileupload. - Revamped styles in styles.ts for improved design consistency and responsiveness. - Enhanced file upload functionality with better descriptions and validation messages.
This commit is contained in:
@@ -1,15 +1,15 @@
|
|||||||
import { DeesContextmenu } from '../dees-contextmenu.js';
|
|
||||||
import { DeesInputBase } from '../dees-input-base.js';
|
import { DeesInputBase } from '../dees-input-base.js';
|
||||||
import { demoFunc } from './demo.js';
|
import { demoFunc } from './demo.js';
|
||||||
import { fileuploadStyles } from './styles.js';
|
import { fileuploadStyles } from './styles.js';
|
||||||
import { renderFileupload } from './template.js';
|
|
||||||
import '../dees-icon.js';
|
import '../dees-icon.js';
|
||||||
import '../dees-label.js';
|
import '../dees-label.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
type TemplateResult,
|
html,
|
||||||
property,
|
property,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -22,22 +22,17 @@ declare global {
|
|||||||
export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||||
public static demo = demoFunc;
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
@property({
|
|
||||||
attribute: false,
|
|
||||||
})
|
|
||||||
public value: File[] = [];
|
public value: File[] = [];
|
||||||
|
|
||||||
@property()
|
@state()
|
||||||
public state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle';
|
public state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle';
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@state()
|
||||||
public isLoading: boolean = false;
|
public isLoading: boolean = false;
|
||||||
|
|
||||||
@property({
|
@property({ type: String })
|
||||||
type: String,
|
public buttonText: string = 'Select files';
|
||||||
})
|
|
||||||
public buttonText: string = 'Upload File...';
|
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
public accept: string = '';
|
public accept: string = '';
|
||||||
@@ -54,24 +49,258 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
@property({ type: String, reflect: true })
|
@property({ type: String, reflect: true })
|
||||||
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
|
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
|
||||||
|
|
||||||
constructor() {
|
public validationMessage: string = '';
|
||||||
super();
|
|
||||||
}
|
private previewUrlMap: WeakMap<File, string> = new WeakMap();
|
||||||
|
private dropArea: HTMLElement | null = null;
|
||||||
|
|
||||||
public static styles = fileuploadStyles;
|
public static styles = fileuploadStyles;
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return renderFileupload(this);
|
const acceptedSummary = this.getAcceptedSummary();
|
||||||
|
const metaEntries: string[] = [
|
||||||
|
this.multiple ? 'Multiple files supported' : 'Single file only',
|
||||||
|
this.maxSize > 0 ? `Max ${this.formatFileSize(this.maxSize)}` : 'No size limit',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (acceptedSummary) {
|
||||||
|
metaEntries.push(`Accepts ${acceptedSummary}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<dees-label
|
||||||
|
.label=${this.label}
|
||||||
|
.description=${this.description}
|
||||||
|
.required=${this.required}
|
||||||
|
></dees-label>
|
||||||
|
<div
|
||||||
|
class="dropzone ${this.state === 'dragOver' ? 'dropzone--active' : ''} ${this.disabled ? 'dropzone--disabled' : ''}"
|
||||||
|
role="button"
|
||||||
|
tabindex=${this.disabled ? -1 : 0}
|
||||||
|
aria-disabled=${this.disabled}
|
||||||
|
aria-label=${`Select files${acceptedSummary ? ` (${acceptedSummary})` : ''}`}
|
||||||
|
@click=${this.handleDropzoneClick}
|
||||||
|
@keydown=${this.handleDropzoneKeydown}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="file-input"
|
||||||
|
style="position: absolute; opacity: 0; pointer-events: none; width: 1px; height: 1px; top: 0; left: 0; overflow: hidden;"
|
||||||
|
type="file"
|
||||||
|
?multiple=${this.multiple}
|
||||||
|
accept=${this.accept || ''}
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
@change=${this.handleFileInputChange}
|
||||||
|
tabindex="-1"
|
||||||
|
/>
|
||||||
|
<div class="dropzone__body">
|
||||||
|
<div class="dropzone__icon">
|
||||||
|
${this.isLoading
|
||||||
|
? html`<span class="dropzone__loader" aria-hidden="true"></span>`
|
||||||
|
: html`<dees-icon icon="lucide:FolderOpen"></dees-icon>`}
|
||||||
|
</div>
|
||||||
|
<div class="dropzone__content">
|
||||||
|
<span class="dropzone__headline">${this.buttonText || 'Select files'}</span>
|
||||||
|
<span class="dropzone__subline">
|
||||||
|
Drag and drop files here or
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dropzone__browse"
|
||||||
|
@click=${this.handleBrowseClick}
|
||||||
|
?disabled=${this.disabled}
|
||||||
|
>
|
||||||
|
browse
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dropzone__meta">
|
||||||
|
${metaEntries.map((entry) => html`<span>${entry}</span>`)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${this.renderFileList()}
|
||||||
|
${this.validationMessage
|
||||||
|
? html`<div class="validation-message" aria-live="polite">${this.validationMessage}</div>`
|
||||||
|
: html``}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public validationMessage: string = '';
|
private renderFileList(): TemplateResult {
|
||||||
|
if (this.value.length === 0) {
|
||||||
|
return html`
|
||||||
|
<div class="file-empty">
|
||||||
|
<div class="file-empty__title">No files selected</div>
|
||||||
|
<div class="file-empty__hint">
|
||||||
|
Selected files will be listed here
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file-list__header">
|
||||||
|
<span>${this.value.length} file${this.value.length === 1 ? '' : 's'} selected</span>
|
||||||
|
${this.value.length > 0
|
||||||
|
? html`<button type="button" class="file-list__clear" @click=${this.handleClearAll}>Clear ${this.value.length > 1 ? 'all' : ''}</button>`
|
||||||
|
: html``}
|
||||||
|
</div>
|
||||||
|
<div class="file-list__items">
|
||||||
|
${this.value.map((file) => this.renderFileRow(file))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFileRow(file: File): TemplateResult {
|
||||||
|
const fileType = this.getFileType(file);
|
||||||
|
const previewUrl = this.canShowPreview(file) ? this.getPreviewUrl(file) : null;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="file-row ${fileType}-file">
|
||||||
|
<div class="file-thumb" aria-hidden="true">
|
||||||
|
${previewUrl
|
||||||
|
? html`<img class="thumb-image" src=${previewUrl} alt=${`Preview of ${file.name}`}>`
|
||||||
|
: html`<dees-icon icon=${this.getFileIcon(file)}></dees-icon>`}
|
||||||
|
</div>
|
||||||
|
<div class="file-meta">
|
||||||
|
<div class="file-name" title=${file.name}>${file.name}</div>
|
||||||
|
<div class="file-details">
|
||||||
|
<span class="file-size">${this.formatFileSize(file.size)}</span>
|
||||||
|
${fileType !== 'file' ? html`<span class="file-type">${fileType}</span>` : html``}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="remove-button"
|
||||||
|
@click=${() => this.removeFile(file)}
|
||||||
|
aria-label=${`Remove ${file.name}`}
|
||||||
|
>
|
||||||
|
<dees-icon icon="lucide:X"></dees-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFileInputChange = async (event: Event) => {
|
||||||
|
this.isLoading = false;
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const files = Array.from(target.files ?? []);
|
||||||
|
if (files.length > 0) {
|
||||||
|
await this.addFiles(files);
|
||||||
|
}
|
||||||
|
target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleDropzoneClick = (event: MouseEvent) => {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Don't prevent default - let the click bubble
|
||||||
|
if ((event.target as HTMLElement).closest('.dropzone__browse')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.openFileSelector();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleBrowseClick = (event: MouseEvent) => {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.stopPropagation(); // Stop propagation to prevent double trigger
|
||||||
|
this.openFileSelector();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleDropzoneKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.openFileSelector();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleClearAll = (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.clearAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleDragEvent = async (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'dragenter' || event.type === 'dragover') {
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.dropEffect = 'copy';
|
||||||
|
}
|
||||||
|
this.state = 'dragOver';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'dragleave') {
|
||||||
|
if (!this.dropArea) {
|
||||||
|
this.state = 'idle';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = this.dropArea.getBoundingClientRect();
|
||||||
|
const { clientX = 0, clientY = 0 } = event;
|
||||||
|
if (clientX <= rect.left || clientX >= rect.right || clientY <= rect.top || clientY >= rect.bottom) {
|
||||||
|
this.state = 'idle';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'drop') {
|
||||||
|
this.state = 'idle';
|
||||||
|
const files = Array.from(event.dataTransfer?.files ?? []);
|
||||||
|
if (files.length > 0) {
|
||||||
|
await this.addFiles(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private attachDropListeners(): void {
|
||||||
|
if (!this.dropArea) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
|
||||||
|
this.dropArea!.addEventListener(eventName, this.handleDragEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private detachDropListeners(): void {
|
||||||
|
if (!this.dropArea) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
|
||||||
|
this.dropArea!.removeEventListener(eventName, this.handleDragEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private rebindInteractiveElements(): void {
|
||||||
|
const newDropArea = this.shadowRoot?.querySelector('.dropzone') as HTMLElement | null;
|
||||||
|
|
||||||
|
if (newDropArea !== this.dropArea) {
|
||||||
|
this.detachDropListeners();
|
||||||
|
this.dropArea = newDropArea;
|
||||||
|
this.attachDropListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Utility methods
|
|
||||||
public formatFileSize(bytes: number): string {
|
public formatFileSize(bytes: number): string {
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
const units = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
||||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
const size = bytes / Math.pow(1024, exponent);
|
||||||
|
return `${Math.round(size * 100) / 100} ${units[exponent]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFileType(file: File): string {
|
public getFileType(file: File): string {
|
||||||
@@ -88,81 +317,187 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getFileIcon(file: File): string {
|
public getFileIcon(file: File): string {
|
||||||
const type = this.getFileType(file);
|
const fileType = this.getFileType(file);
|
||||||
const iconMap = {
|
const iconMap: Record<string, string> = {
|
||||||
'image': 'lucide:image',
|
image: 'lucide:FileImage',
|
||||||
'pdf': 'lucide:file-text',
|
pdf: 'lucide:FileText',
|
||||||
'doc': 'lucide:file-text',
|
doc: 'lucide:FileText',
|
||||||
'spreadsheet': 'lucide:table',
|
spreadsheet: 'lucide:FileSpreadsheet',
|
||||||
'presentation': 'lucide:presentation',
|
presentation: 'lucide:FileBarChart',
|
||||||
'video': 'lucide:video',
|
video: 'lucide:FileVideo',
|
||||||
'audio': 'lucide:music',
|
audio: 'lucide:FileAudio',
|
||||||
'archive': 'lucide:archive',
|
archive: 'lucide:FileArchive',
|
||||||
'file': 'lucide:file'
|
file: 'lucide:File',
|
||||||
};
|
};
|
||||||
return iconMap[type] || 'lucide:file';
|
return iconMap[fileType] ?? 'lucide:File';
|
||||||
}
|
}
|
||||||
|
|
||||||
public canShowPreview(file: File): boolean {
|
public canShowPreview(file: File): boolean {
|
||||||
return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024; // 5MB limit for previews
|
return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateFile(file: File): boolean {
|
private validateFile(file: File): boolean {
|
||||||
// Check file size
|
|
||||||
if (this.maxSize > 0 && file.size > this.maxSize) {
|
if (this.maxSize > 0 && file.size > this.maxSize) {
|
||||||
this.validationMessage = `File "${file.name}" exceeds maximum size of ${this.formatFileSize(this.maxSize)}`;
|
this.validationMessage = `File "${file.name}" exceeds the maximum size of ${this.formatFileSize(this.maxSize)}`;
|
||||||
this.validationState = 'invalid';
|
this.validationState = 'invalid';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check file type
|
|
||||||
if (this.accept) {
|
if (this.accept) {
|
||||||
const acceptedTypes = this.accept.split(',').map(s => s.trim());
|
const acceptedTypes = this.accept
|
||||||
let isAccepted = false;
|
.split(',')
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter((entry) => entry.length > 0);
|
||||||
|
|
||||||
for (const acceptType of acceptedTypes) {
|
if (acceptedTypes.length > 0) {
|
||||||
if (acceptType.startsWith('.')) {
|
let isAccepted = false;
|
||||||
// Extension check
|
for (const acceptType of acceptedTypes) {
|
||||||
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) {
|
if (acceptType.startsWith('.')) {
|
||||||
|
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) {
|
||||||
|
isAccepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (acceptType.endsWith('/*')) {
|
||||||
|
const prefix = acceptType.slice(0, -2);
|
||||||
|
if (file.type.startsWith(prefix)) {
|
||||||
|
isAccepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (file.type === acceptType) {
|
||||||
isAccepted = true;
|
isAccepted = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if (acceptType.endsWith('/*')) {
|
|
||||||
// MIME type wildcard check
|
|
||||||
const mimePrefix = acceptType.slice(0, -2);
|
|
||||||
if (file.type.startsWith(mimePrefix)) {
|
|
||||||
isAccepted = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (file.type === acceptType) {
|
|
||||||
// Exact MIME type check
|
|
||||||
isAccepted = true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAccepted) {
|
if (!isAccepted) {
|
||||||
this.validationMessage = `File type not accepted. Please upload: ${this.accept}`;
|
this.validationMessage = `File type not accepted. Allowed: ${acceptedTypes.join(', ')}`;
|
||||||
this.validationState = 'invalid';
|
this.validationState = 'invalid';
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openFileSelector() {
|
private getPreviewUrl(file: File): string {
|
||||||
if (this.disabled || this.isLoading) return;
|
let url = this.previewUrlMap.get(file);
|
||||||
|
if (!url) {
|
||||||
|
url = URL.createObjectURL(file);
|
||||||
|
this.previewUrlMap.set(file, url);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private releasePreview(file: File): void {
|
||||||
|
const url = this.previewUrlMap.get(file);
|
||||||
|
if (url) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
this.previewUrlMap.delete(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAcceptedSummary(): string | null {
|
||||||
|
if (!this.accept) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = Array.from(
|
||||||
|
new Set(
|
||||||
|
this.accept
|
||||||
|
.split(',')
|
||||||
|
.map((token) => token.trim())
|
||||||
|
.filter((token) => token.length > 0)
|
||||||
|
.map((token) => this.formatAcceptToken(token))
|
||||||
|
)
|
||||||
|
).filter(Boolean);
|
||||||
|
|
||||||
|
if (formatted.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formatted.length === 1) {
|
||||||
|
return formatted[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formatted.length === 2) {
|
||||||
|
return `${formatted[0]}, ${formatted[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${formatted.slice(0, 2).join(', ')}…`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatAcceptToken(token: string): string {
|
||||||
|
if (token === '*/*') {
|
||||||
|
return 'All files';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.endsWith('/*')) {
|
||||||
|
const family = token.split('/')[0];
|
||||||
|
if (!family) {
|
||||||
|
return 'All files';
|
||||||
|
}
|
||||||
|
return `${family.charAt(0).toUpperCase()}${family.slice(1)} files`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.startsWith('.')) {
|
||||||
|
return token.slice(1).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.includes('pdf')) return 'PDF';
|
||||||
|
if (token.includes('zip')) return 'ZIP';
|
||||||
|
if (token.includes('json')) return 'JSON';
|
||||||
|
if (token.includes('msword')) return 'DOC';
|
||||||
|
if (token.includes('wordprocessingml')) return 'DOCX';
|
||||||
|
if (token.includes('excel')) return 'XLS';
|
||||||
|
if (token.includes('presentation')) return 'PPT';
|
||||||
|
|
||||||
|
const segments = token.split('/');
|
||||||
|
const lastSegment = segments.pop() ?? token;
|
||||||
|
return lastSegment.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachLifecycleListeners(): void {
|
||||||
|
this.rebindInteractiveElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
public firstUpdated(changedProperties: Map<string, unknown>) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
this.attachLifecycleListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
public updated(changedProperties: Map<string, unknown>) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
if (changedProperties.has('value')) {
|
||||||
|
void this.validate();
|
||||||
|
}
|
||||||
|
this.rebindInteractiveElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnectedCallback(): Promise<void> {
|
||||||
|
this.detachDropListeners();
|
||||||
|
this.value.forEach((file) => this.releasePreview(file));
|
||||||
|
this.previewUrlMap = new WeakMap();
|
||||||
|
await super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async openFileSelector() {
|
||||||
|
if (this.disabled || this.isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Set loading state
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
|
// Ensure we have the latest reference to the file input
|
||||||
|
const inputFile = this.shadowRoot?.querySelector('.file-input') as HTMLInputElement | null;
|
||||||
|
|
||||||
|
if (!inputFile) {
|
||||||
|
this.isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Set up a focus handler to detect when the dialog is closed without selection
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Check if no file was selected
|
|
||||||
if (!inputFile.files || inputFile.files.length === 0) {
|
if (!inputFile.files || inputFile.files.length === 0) {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
@@ -171,79 +506,52 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('focus', handleFocus);
|
window.addEventListener('focus', handleFocus);
|
||||||
|
|
||||||
|
// Click the input to open file selector
|
||||||
inputFile.click();
|
inputFile.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeFile(file: File) {
|
public removeFile(file: File) {
|
||||||
const index = this.value.indexOf(file);
|
const index = this.value.indexOf(file);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
|
this.releasePreview(file);
|
||||||
this.value.splice(index, 1);
|
this.value.splice(index, 1);
|
||||||
this.requestUpdate();
|
this.requestUpdate('value');
|
||||||
this.validate();
|
void this.validate();
|
||||||
this.changeSubject.next(this);
|
this.changeSubject.next(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearAll() {
|
public clearAll() {
|
||||||
|
const existingFiles = [...this.value];
|
||||||
this.value = [];
|
this.value = [];
|
||||||
this.requestUpdate();
|
existingFiles.forEach((file) => this.releasePreview(file));
|
||||||
this.validate();
|
this.requestUpdate('value');
|
||||||
|
void this.validate();
|
||||||
this.changeSubject.next(this);
|
this.changeSubject.next(this);
|
||||||
|
this.buttonText = 'Select files';
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateValue(eventArg: Event) {
|
public async updateValue(eventArg: Event) {
|
||||||
const target: any = eventArg.target;
|
const target = eventArg.target as HTMLInputElement;
|
||||||
this.value = target.value;
|
this.value = Array.from(target.files ?? []);
|
||||||
this.changeSubject.next(this);
|
this.changeSubject.next(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
public setValue(value: File[]): void {
|
||||||
super.firstUpdated(_changedProperties);
|
this.value.forEach((file) => this.releasePreview(file));
|
||||||
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
|
this.value = value;
|
||||||
inputFile.addEventListener('change', async (event: Event) => {
|
if (value.length > 0) {
|
||||||
const target = event.target as HTMLInputElement;
|
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
|
||||||
const newFiles = Array.from(target.files);
|
} else {
|
||||||
|
this.buttonText = 'Select files';
|
||||||
|
}
|
||||||
|
this.requestUpdate('value');
|
||||||
|
void this.validate();
|
||||||
|
}
|
||||||
|
|
||||||
// Always reset loading state when file dialog interaction completes
|
public getValue(): File[] {
|
||||||
this.isLoading = false;
|
return this.value;
|
||||||
|
|
||||||
await this.addFiles(newFiles);
|
|
||||||
// Reset the input value to allow selecting the same file again if needed
|
|
||||||
target.value = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle drag and drop
|
|
||||||
const dropArea = this.shadowRoot.querySelector('.maincontainer');
|
|
||||||
const handlerFunction = async (eventArg: DragEvent) => {
|
|
||||||
eventArg.preventDefault();
|
|
||||||
eventArg.stopPropagation();
|
|
||||||
|
|
||||||
switch (eventArg.type) {
|
|
||||||
case 'dragenter':
|
|
||||||
case 'dragover':
|
|
||||||
this.state = 'dragOver';
|
|
||||||
break;
|
|
||||||
case 'dragleave':
|
|
||||||
// Check if we're actually leaving the drop area
|
|
||||||
const rect = dropArea.getBoundingClientRect();
|
|
||||||
const x = eventArg.clientX;
|
|
||||||
const y = eventArg.clientY;
|
|
||||||
if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
|
|
||||||
this.state = 'idle';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'drop':
|
|
||||||
this.state = 'idle';
|
|
||||||
const files = Array.from(eventArg.dataTransfer.files);
|
|
||||||
await this.addFiles(files);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
dropArea.addEventListener('dragenter', handlerFunction, false);
|
|
||||||
dropArea.addEventListener('dragleave', handlerFunction, false);
|
|
||||||
dropArea.addEventListener('dragover', handlerFunction, false);
|
|
||||||
dropArea.addEventListener('drop', handlerFunction, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async addFiles(files: File[]) {
|
private async addFiles(files: File[]) {
|
||||||
@@ -255,9 +563,11 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filesToAdd.length === 0) return;
|
if (filesToAdd.length === 0) {
|
||||||
|
this.isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check max files limit
|
|
||||||
if (this.maxFiles > 0) {
|
if (this.maxFiles > 0) {
|
||||||
const totalFiles = this.value.length + filesToAdd.length;
|
const totalFiles = this.value.length + filesToAdd.length;
|
||||||
if (totalFiles > this.maxFiles) {
|
if (totalFiles > this.maxFiles) {
|
||||||
@@ -265,6 +575,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
if (allowedCount <= 0) {
|
if (allowedCount <= 0) {
|
||||||
this.validationMessage = `Maximum ${this.maxFiles} files allowed`;
|
this.validationMessage = `Maximum ${this.maxFiles} files allowed`;
|
||||||
this.validationState = 'invalid';
|
this.validationState = 'invalid';
|
||||||
|
this.isLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
filesToAdd.splice(allowedCount);
|
filesToAdd.splice(allowedCount);
|
||||||
@@ -273,20 +584,24 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add files
|
|
||||||
if (!this.multiple && filesToAdd.length > 0) {
|
if (!this.multiple && filesToAdd.length > 0) {
|
||||||
|
this.value.forEach((file) => this.releasePreview(file));
|
||||||
this.value = [filesToAdd[0]];
|
this.value = [filesToAdd[0]];
|
||||||
} else {
|
} else {
|
||||||
this.value.push(...filesToAdd);
|
this.value.push(...filesToAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.requestUpdate();
|
this.validationMessage = '';
|
||||||
this.validate();
|
this.validationState = null;
|
||||||
|
this.requestUpdate('value');
|
||||||
|
await this.validate();
|
||||||
this.changeSubject.next(this);
|
this.changeSubject.next(this);
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
// Update button text
|
|
||||||
if (this.value.length > 0) {
|
if (this.value.length > 0) {
|
||||||
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
|
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
|
||||||
|
} else {
|
||||||
|
this.buttonText = 'Select files';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,36 +614,13 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate all files
|
|
||||||
for (const file of this.value) {
|
for (const file of this.value) {
|
||||||
if (!this.validateFile(file)) {
|
if (!this.validateFile(file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.validationState = 'valid';
|
this.validationState = this.value.length > 0 ? 'valid' : null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getValue(): File[] {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setValue(value: File[]): void {
|
|
||||||
this.value = value;
|
|
||||||
this.requestUpdate();
|
|
||||||
if (value.length > 0) {
|
|
||||||
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
|
|
||||||
} else {
|
|
||||||
this.buttonText = 'Upload File...';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public updated(changedProperties: Map<string, any>) {
|
|
||||||
super.updated(changedProperties);
|
|
||||||
|
|
||||||
if (changedProperties.has('value')) {
|
|
||||||
this.validate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
import { css, cssManager, html } from '@design.estate/dees-element';
|
||||||
import './component.js';
|
import './component.js';
|
||||||
import '../dees-panel.js';
|
import '../dees-panel.js';
|
||||||
|
|
||||||
@@ -6,199 +6,152 @@ export const demoFunc = () => html`
|
|||||||
<dees-demowrapper>
|
<dees-demowrapper>
|
||||||
<style>
|
<style>
|
||||||
${css`
|
${css`
|
||||||
.demo-container {
|
.demo-shell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 32px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
max-width: 1200px;
|
max-width: 1160px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-grid {
|
.demo-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (min-width: 960px) {
|
||||||
.upload-grid {
|
.demo-grid--two {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-box {
|
.demo-stack {
|
||||||
padding: 16px;
|
display: flex;
|
||||||
background: ${cssManager.bdTheme('#fff', '#2a2a2a')};
|
flex-direction: column;
|
||||||
border-radius: 4px;
|
gap: 18px;
|
||||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444')};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-box h4 {
|
.demo-note {
|
||||||
margin-top: 0;
|
margin-top: 16px;
|
||||||
margin-bottom: 16px;
|
padding: 16px;
|
||||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
border-radius: 12px;
|
||||||
font-size: 16px;
|
border: 1px solid ${cssManager.bdTheme('hsl(217 91% 90%)', 'hsl(215 20% 26%)')};
|
||||||
|
background: ${cssManager.bdTheme('hsl(213 100% 97%)', 'hsl(215 20% 12%)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 25% 32%)', 'hsl(215 20% 82%)')};
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-section {
|
.demo-note strong {
|
||||||
margin-top: 32px;
|
color: ${cssManager.bdTheme('hsl(217 91% 45%)', 'hsl(213 93% 68%)')};
|
||||||
padding: 16px;
|
font-weight: 600;
|
||||||
background: ${cssManager.bdTheme('#fff3cd', '#332701')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#ffeaa7', '#664400')};
|
|
||||||
border-radius: 4px;
|
|
||||||
color: ${cssManager.bdTheme('#856404', '#ffecb5')};
|
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="demo-container">
|
<div class="demo-shell">
|
||||||
<dees-panel .title=${'1. Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}>
|
<dees-panel
|
||||||
<dees-input-fileupload
|
.title=${'Modern file uploader'}
|
||||||
.label=${'Attachments'}
|
.subtitle=${'Shadcn-inspired layout with drag & drop, previews and validation'}
|
||||||
.description=${'Upload any files by clicking or dragging them here'}
|
>
|
||||||
></dees-input-fileupload>
|
<div class="demo-grid demo-grid--two">
|
||||||
|
<div class="demo-stack">
|
||||||
<dees-input-fileupload
|
|
||||||
.label=${'Single File Only'}
|
|
||||||
.description=${'Only one file can be uploaded at a time'}
|
|
||||||
.multiple=${false}
|
|
||||||
.buttonText=${'Choose File'}
|
|
||||||
></dees-input-fileupload>
|
|
||||||
</dees-panel>
|
|
||||||
|
|
||||||
<dees-panel .title=${'2. File Type Restrictions'} .subtitle=${'Upload areas with specific file type requirements'}>
|
|
||||||
<div class="upload-grid">
|
|
||||||
<div class="upload-box">
|
|
||||||
<h4>Images Only</h4>
|
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Profile Picture'}
|
.label=${'Attachments'}
|
||||||
.description=${'JPG, PNG or GIF (max 5MB)'}
|
.description=${'Upload supporting documents for your request'}
|
||||||
.accept=${'image/jpeg,image/png,image/gif'}
|
.accept=${'image/*,.pdf,.zip'}
|
||||||
.maxSize=${5 * 1024 * 1024}
|
.maxSize=${10 * 1024 * 1024}
|
||||||
|
></dees-input-fileupload>
|
||||||
|
|
||||||
|
<dees-input-fileupload
|
||||||
|
.label=${'Brand assets'}
|
||||||
|
.description=${'Upload high-resolution imagery (JPG/PNG)'}
|
||||||
|
.accept=${'image/jpeg,image/png'}
|
||||||
.multiple=${false}
|
.multiple=${false}
|
||||||
.buttonText=${'Select Image'}
|
.maxSize=${5 * 1024 * 1024}
|
||||||
|
.buttonText=${'Select cover image'}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="upload-box">
|
<div class="demo-stack">
|
||||||
<h4>Documents Only</h4>
|
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Resume'}
|
.label=${'Audio uploads'}
|
||||||
.description=${'PDF or Word documents only'}
|
.description=${'Share podcast drafts (MP3/WAV, max 25MB each)'}
|
||||||
.accept=${".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"}
|
.accept=${'audio/*'}
|
||||||
.buttonText=${'Select Document'}
|
.maxSize=${25 * 1024 * 1024}
|
||||||
|
></dees-input-fileupload>
|
||||||
|
|
||||||
|
<dees-input-fileupload
|
||||||
|
.label=${'Disabled example'}
|
||||||
|
.description=${'Uploader is disabled while moderation is pending'}
|
||||||
|
.disabled=${true}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dees-panel>
|
</dees-panel>
|
||||||
|
|
||||||
<dees-panel .title=${'3. Validation & Limits'} .subtitle=${'File size limits and validation examples'}>
|
<dees-panel
|
||||||
<dees-input-fileupload
|
.title=${'Form integration'}
|
||||||
.label=${'Small Files Only'}
|
.subtitle=${'Combine file uploads with the rest of the DEES form ecosystem'}
|
||||||
.description=${'Maximum file size: 1MB'}
|
>
|
||||||
.maxSize=${1024 * 1024}
|
<div class="demo-grid">
|
||||||
.buttonText=${'Upload Small File'}
|
<dees-form>
|
||||||
></dees-input-fileupload>
|
<div class="demo-stack">
|
||||||
|
<dees-input-text
|
||||||
|
.label=${'Project name'}
|
||||||
|
.description=${'How should we refer to this project internally?'}
|
||||||
|
.required=${true}
|
||||||
|
.key=${'projectName'}
|
||||||
|
></dees-input-text>
|
||||||
|
|
||||||
<dees-input-fileupload
|
<dees-input-text
|
||||||
.label=${'Limited Upload'}
|
.label=${'Contact email'}
|
||||||
.description=${'Maximum 3 files, each up to 2MB'}
|
.inputType=${'email'}
|
||||||
.maxFiles=${3}
|
.required=${true}
|
||||||
.maxSize=${2 * 1024 * 1024}
|
.key=${'contactEmail'}
|
||||||
></dees-input-fileupload>
|
></dees-input-text>
|
||||||
|
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Required Upload'}
|
.label=${'Statement of work'}
|
||||||
.description=${'This field is required'}
|
.description=${'Upload a signed statement of work (PDF, max 15MB)'}
|
||||||
.required=${true}
|
.required=${true}
|
||||||
></dees-input-fileupload>
|
.accept=${'application/pdf'}
|
||||||
</dees-panel>
|
.maxSize=${15 * 1024 * 1024}
|
||||||
|
.multiple=${false}
|
||||||
|
.key=${'sow'}
|
||||||
|
></dees-input-fileupload>
|
||||||
|
|
||||||
<dees-panel .title=${'4. States & Styling'} .subtitle=${'Different states and validation feedback'}>
|
<dees-input-fileupload
|
||||||
<dees-input-fileupload
|
.label=${'Creative references'}
|
||||||
.label=${'Disabled Upload'}
|
.description=${'Optional. Upload up to five visual references'}
|
||||||
.description=${'File upload is currently disabled'}
|
.accept=${'image/*'}
|
||||||
.disabled=${true}
|
.maxFiles=${5}
|
||||||
></dees-input-fileupload>
|
.maxSize=${8 * 1024 * 1024}
|
||||||
|
.key=${'references'}
|
||||||
|
></dees-input-fileupload>
|
||||||
|
|
||||||
<dees-input-fileupload
|
<dees-input-text
|
||||||
.label=${'Pre-filled Example'}
|
.label=${'Notes'}
|
||||||
.description=${'Component with pre-loaded files'}
|
.description=${'Add optional context for reviewers'}
|
||||||
.value=${[
|
.inputType=${'textarea'}
|
||||||
new File(['Hello World'], 'example.txt', { type: 'text/plain' }),
|
.key=${'notes'}
|
||||||
new File(['Test Data'], 'data.json', { type: 'application/json' })
|
></dees-input-text>
|
||||||
]}
|
|
||||||
></dees-input-fileupload>
|
|
||||||
</dees-panel>
|
|
||||||
|
|
||||||
<dees-panel .title=${'5. Form Integration'} .subtitle=${'Complete form with various file upload scenarios'}>
|
<dees-form-submit .text=${'Submit briefing'}></dees-form-submit>
|
||||||
<dees-form>
|
</div>
|
||||||
<h3 style="margin-top: 0; margin-bottom: 24px; color: ${cssManager.bdTheme('#333', '#fff')};">Job Application Form</h3>
|
</dees-form>
|
||||||
|
|
||||||
<dees-input-text
|
<div class="demo-note">
|
||||||
.label=${'Full Name'}
|
<strong>Good to know:</strong>
|
||||||
.required=${true}
|
<ul>
|
||||||
.key=${'fullName'}
|
<li>Drag & drop highlights the dropzone and supports keyboard activation.</li>
|
||||||
></dees-input-text>
|
<li>Accepted file types are summarised automatically from the <code>accept</code> attribute.</li>
|
||||||
|
<li>Image uploads show live previews generated via <code>URL.createObjectURL</code>.</li>
|
||||||
<dees-input-text
|
<li>File size and file-count limits surface inline validation messages.</li>
|
||||||
.label=${'Email'}
|
<li>The component stays compatible with <code>dees-form</code> value accessors.</li>
|
||||||
.inputType=${'email'}
|
</ul>
|
||||||
.required=${true}
|
</div>
|
||||||
.key=${'email'}
|
|
||||||
></dees-input-text>
|
|
||||||
|
|
||||||
<dees-input-fileupload
|
|
||||||
.label=${'Resume'}
|
|
||||||
.description=${'Required: PDF format only (max 10MB)'}
|
|
||||||
.required=${true}
|
|
||||||
.accept=${'application/pdf'}
|
|
||||||
.maxSize=${10 * 1024 * 1024}
|
|
||||||
.multiple=${false}
|
|
||||||
.key=${'resume'}
|
|
||||||
></dees-input-fileupload>
|
|
||||||
|
|
||||||
<dees-input-fileupload
|
|
||||||
.label=${'Portfolio'}
|
|
||||||
.description=${'Optional: Upload up to 5 work samples (images or PDFs, max 5MB each)'}
|
|
||||||
.accept=${'image/*,application/pdf'}
|
|
||||||
.maxFiles=${5}
|
|
||||||
.maxSize=${5 * 1024 * 1024}
|
|
||||||
.key=${'portfolio'}
|
|
||||||
></dees-input-fileupload>
|
|
||||||
|
|
||||||
<dees-input-fileupload
|
|
||||||
.label=${'References'}
|
|
||||||
.description=${'Upload reference letters (optional)'}
|
|
||||||
.accept=${".pdf,.doc,.docx"}
|
|
||||||
.key=${'references'}
|
|
||||||
></dees-input-fileupload>
|
|
||||||
|
|
||||||
<dees-input-text
|
|
||||||
.label=${'Additional Comments'}
|
|
||||||
.inputType=${'textarea'}
|
|
||||||
.description=${'Any additional information you would like to share'}
|
|
||||||
.key=${'comments'}
|
|
||||||
></dees-input-text>
|
|
||||||
|
|
||||||
<dees-form-submit .text=${'Submit Application'}></dees-form-submit>
|
|
||||||
</dees-form>
|
|
||||||
|
|
||||||
<div class="info-section">
|
|
||||||
<h4 style="margin-top: 0;">Enhanced Features:</h4>
|
|
||||||
<ul style="margin: 0; padding-left: 20px;">
|
|
||||||
<li>Drag & drop with visual feedback</li>
|
|
||||||
<li>File type restrictions via accept attribute</li>
|
|
||||||
<li>File size validation with custom limits</li>
|
|
||||||
<li>Maximum file count restrictions</li>
|
|
||||||
<li>Image preview thumbnails</li>
|
|
||||||
<li>File type-specific icons</li>
|
|
||||||
<li>Clear all button for multiple files</li>
|
|
||||||
<li>Proper validation states and messages</li>
|
|
||||||
<li>Keyboard accessible</li>
|
|
||||||
<li>Single or multiple file modes</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</dees-panel>
|
</dees-panel>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,3 +1,2 @@
|
|||||||
export * from './component.js';
|
export * from './component.js';
|
||||||
export { fileuploadStyles } from './styles.js';
|
export { fileuploadStyles } from './styles.js';
|
||||||
export { renderFileupload } from './template.js';
|
|
||||||
|
@@ -2,308 +2,332 @@ import { css, cssManager } from '@design.estate/dees-element';
|
|||||||
import { DeesInputBase } from '../dees-input-base.js';
|
import { DeesInputBase } from '../dees-input-base.js';
|
||||||
|
|
||||||
export const fileuploadStyles = [
|
export const fileuploadStyles = [
|
||||||
...DeesInputBase.baseStyles,
|
...DeesInputBase.baseStyles,
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maincontainer {
|
.dropzone {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 6px;
|
padding: 20px;
|
||||||
padding: 16px;
|
border-radius: 12px;
|
||||||
background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 11.8%)')};
|
border: 1.5px dashed ${cssManager.bdTheme('hsl(215 16% 80%)', 'hsl(217 20% 25%)')};
|
||||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 12%)')};
|
||||||
border: 1px solid ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')};
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||||
transition: all 0.15s ease;
|
cursor: pointer;
|
||||||
}
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.maincontainer:hover {
|
.dropzone:focus-visible {
|
||||||
border-color: ${cssManager.bdTheme('hsl(215 20.2% 55.1%)', 'hsl(215 20.2% 45.1%)')};
|
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 12%)')},
|
||||||
}
|
0 0 0 4px ${cssManager.bdTheme('hsl(217 91% 60% / 0.5)', 'hsl(213 93% 68% / 0.4)')};
|
||||||
|
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||||
|
}
|
||||||
|
|
||||||
:host([disabled]) .maincontainer {
|
.dropzone--active {
|
||||||
opacity: 0.5;
|
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||||
cursor: not-allowed;
|
box-shadow: 0 12px 32px ${cssManager.bdTheme('rgba(15, 23, 42, 0.12)', 'rgba(0, 0, 0, 0.35)')};
|
||||||
pointer-events: none;
|
background: ${cssManager.bdTheme('hsl(217 91% 60% / 0.06)', 'hsl(213 93% 68% / 0.12)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([validationState="invalid"]) .maincontainer {
|
.dropzone--disabled {
|
||||||
border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
opacity: 0.6;
|
||||||
}
|
pointer-events: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
:host([validationState="valid"]) .maincontainer {
|
.dropzone__body {
|
||||||
border-color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
:host([validationState="warn"]) .maincontainer {
|
.dropzone__icon {
|
||||||
border-color: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
|
width: 48px;
|
||||||
}
|
height: 48px;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||||
|
background: ${cssManager.bdTheme('hsl(217 91% 60% / 0.12)', 'hsl(213 93% 68% / 0.12)')};
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.maincontainer::after {
|
.dropzone__icon dees-icon {
|
||||||
top: 1px;
|
font-size: 22px;
|
||||||
right: 1px;
|
}
|
||||||
left: 1px;
|
|
||||||
bottom: 1px;
|
|
||||||
transform: scale3d(0.98, 0.95, 1);
|
|
||||||
position: absolute;
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
border: 2px dashed transparent;
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
pointer-events: none;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maincontainer.dragOver {
|
.dropzone__loader {
|
||||||
border-color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
width: 20px;
|
||||||
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.05)', 'hsl(213.1 93.9% 67.8% / 0.05)')};
|
height: 20px;
|
||||||
}
|
border-radius: 999px;
|
||||||
|
border: 2px solid ${cssManager.bdTheme('rgba(15, 23, 42, 0.15)', 'rgba(255, 255, 255, 0.15)')};
|
||||||
|
border-top-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||||
|
animation: loader-spin 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.maincontainer.dragOver::after {
|
.dropzone__content {
|
||||||
transform: scale3d(1, 1, 1);
|
display: flex;
|
||||||
border: 2px dashed ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
flex-direction: column;
|
||||||
}
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.uploadButton {
|
.dropzone__headline {
|
||||||
position: relative;
|
font-size: 15px;
|
||||||
padding: 10px 20px;
|
font-weight: 600;
|
||||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 7.8%)')};
|
color: ${cssManager.bdTheme('hsl(222 47% 11%)', 'hsl(210 20% 96%)')};
|
||||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
}
|
||||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
|
||||||
border-radius: 6px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploadButton:hover {
|
.dropzone__subline {
|
||||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
font-size: 13px;
|
||||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadButton:active {
|
.dropzone__browse {
|
||||||
background: ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 11%)')};
|
appearance: none;
|
||||||
}
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.uploadButton dees-icon {
|
.dropzone__browse:hover {
|
||||||
font-size: 16px;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.files-container {
|
.dropzone__browse:disabled {
|
||||||
display: flex;
|
cursor: not-allowed;
|
||||||
flex-direction: column;
|
opacity: 0.6;
|
||||||
gap: 8px;
|
}
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploadCandidate {
|
.dropzone__meta {
|
||||||
display: grid;
|
margin-top: 14px;
|
||||||
grid-template-columns: 40px 1fr auto;
|
display: flex;
|
||||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20.2% 16.8%)')};
|
flex-wrap: wrap;
|
||||||
padding: 12px;
|
gap: 8px;
|
||||||
text-align: left;
|
font-size: 12px;
|
||||||
border-radius: 6px;
|
color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 72%)')};
|
||||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
}
|
||||||
cursor: default;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploadCandidate:hover {
|
.dropzone__meta span {
|
||||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(215 20.2% 20.8%)')};
|
padding: 4px 10px;
|
||||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
border-radius: 999px;
|
||||||
}
|
background: ${cssManager.bdTheme('hsl(217 91% 95%)', 'hsl(213 93% 18%)')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('hsl(217 91% 90%)', 'hsl(213 93% 24%)')};
|
||||||
|
}
|
||||||
|
|
||||||
.uploadCandidate .icon {
|
.file-empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: center;
|
gap: 12px;
|
||||||
font-size: 20px;
|
padding: 16px 0;
|
||||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.uploadCandidate.image-file .icon {
|
.file-empty__title {
|
||||||
color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
|
font-size: 14px;
|
||||||
}
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 16% 40%)', 'hsl(215 16% 70%)')};
|
||||||
|
}
|
||||||
|
|
||||||
.uploadCandidate.pdf-file .icon {
|
.file-empty__hint {
|
||||||
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
font-size: 12px;
|
||||||
}
|
color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 65%)')};
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: ${cssManager.bdTheme('hsl(210 20% 97%)', 'hsl(215 20% 15%)')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('hsl(213 27% 92%)', 'hsl(217 20% 22%)')};
|
||||||
|
}
|
||||||
|
|
||||||
.uploadCandidate.doc-file .icon {
|
.file-list {
|
||||||
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.uploadCandidate .info {
|
.file-list__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 2px;
|
justify-content: space-between;
|
||||||
min-width: 0;
|
font-size: 14px;
|
||||||
}
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 16% 40%)', 'hsl(215 16% 70%)')};
|
||||||
|
}
|
||||||
|
|
||||||
.uploadCandidate .filename {
|
.file-list__clear {
|
||||||
font-weight: 500;
|
appearance: none;
|
||||||
font-size: 14px;
|
border: none;
|
||||||
white-space: nowrap;
|
background: none;
|
||||||
overflow: hidden;
|
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||||
text-overflow: ellipsis;
|
cursor: pointer;
|
||||||
}
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.uploadCandidate .filesize {
|
.file-list__clear:hover {
|
||||||
font-size: 12px;
|
text-decoration: underline;
|
||||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.uploadCandidate .actions {
|
.file-list__items {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove-button {
|
.file-row {
|
||||||
width: 32px;
|
display: flex;
|
||||||
height: 32px;
|
align-items: center;
|
||||||
border-radius: 4px;
|
gap: 16px;
|
||||||
background: transparent;
|
padding: 12px 14px;
|
||||||
border: none;
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 16%)')};
|
||||||
cursor: pointer;
|
border: 1px solid ${cssManager.bdTheme('hsl(213 27% 90%)', 'hsl(217 25% 28%)')};
|
||||||
display: flex;
|
border-radius: 10px;
|
||||||
align-items: center;
|
box-shadow: ${cssManager.bdTheme('0 1px 2px rgba(15, 23, 42, 0.04)', '0 1px 2px rgba(0, 0, 0, 0.24)')};
|
||||||
justify-content: center;
|
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
|
||||||
transition: all 0.15s ease;
|
}
|
||||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-button:hover {
|
.file-row:hover {
|
||||||
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')};
|
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||||
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
box-shadow: ${cssManager.bdTheme('0 10px 24px rgba(15, 23, 42, 0.08)', '0 12px 30px rgba(0, 0, 0, 0.35)')};
|
||||||
}
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
.clear-all-button {
|
.file-thumb {
|
||||||
margin-bottom: 8px;
|
width: 48px;
|
||||||
text-align: right;
|
height: 48px;
|
||||||
}
|
border-radius: 12px;
|
||||||
|
background: ${cssManager.bdTheme('hsl(214 31% 92%)', 'hsl(217 32% 18%)')};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.clear-all-button button {
|
.file-thumb dees-icon {
|
||||||
background: none;
|
font-size: 20px;
|
||||||
border: none;
|
color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 70%)')};
|
||||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
display: block;
|
||||||
cursor: pointer;
|
width: 20px;
|
||||||
font-size: 12px;
|
height: 20px;
|
||||||
padding: 4px 8px;
|
line-height: 1;
|
||||||
border-radius: 4px;
|
flex-shrink: 0;
|
||||||
transition: all 0.15s ease;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.clear-all-button button:hover {
|
|
||||||
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')};
|
|
||||||
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.validation-message {
|
.thumb-image {
|
||||||
font-size: 13px;
|
width: 100%;
|
||||||
margin-top: 6px;
|
height: 100%;
|
||||||
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
object-fit: cover;
|
||||||
line-height: 1.5;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.drop-hint {
|
.file-meta {
|
||||||
text-align: center;
|
display: flex;
|
||||||
padding: 40px 20px;
|
flex-direction: column;
|
||||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
gap: 4px;
|
||||||
font-size: 14px;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-hint dees-icon {
|
.file-name {
|
||||||
font-size: 48px;
|
font-weight: 600;
|
||||||
margin-bottom: 16px;
|
font-size: 14px;
|
||||||
opacity: 0.2;
|
color: ${cssManager.bdTheme('hsl(222 47% 11%)', 'hsl(210 20% 96%)')};
|
||||||
}
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.image-preview {
|
.file-details {
|
||||||
width: 40px;
|
display: flex;
|
||||||
height: 40px;
|
align-items: center;
|
||||||
object-fit: cover;
|
gap: 8px;
|
||||||
border-radius: 4px;
|
flex-wrap: wrap;
|
||||||
}
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')};
|
||||||
|
}
|
||||||
|
|
||||||
.description-text {
|
.file-size {
|
||||||
font-size: 13px;
|
font-variant-numeric: tabular-nums;
|
||||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
}
|
||||||
margin-top: 6px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading state styles */
|
.file-type {
|
||||||
.uploadButton.loading {
|
padding: 2px 8px;
|
||||||
pointer-events: none;
|
border-radius: 999px;
|
||||||
opacity: 0.8;
|
border: 1px solid ${cssManager.bdTheme('hsl(214 31% 86%)', 'hsl(217 32% 28%)')};
|
||||||
}
|
color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')};
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.uploadButton .button-content {
|
.file-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 8px;
|
||||||
gap: 8px;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-spinner {
|
.remove-button {
|
||||||
width: 16px;
|
width: 32px;
|
||||||
height: 16px;
|
height: 32px;
|
||||||
border: 2px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
border-radius: 8px;
|
||||||
border-top-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
background: transparent;
|
||||||
border-radius: 50%;
|
border: none;
|
||||||
animation: spin 0.6s linear infinite;
|
cursor: pointer;
|
||||||
}
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.15s ease, transform 0.15s ease, color 0.15s ease;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 16% 52%)', 'hsl(215 16% 68%)')};
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
.remove-button:hover {
|
||||||
to {
|
background: ${cssManager.bdTheme('hsl(0 72% 50% / 0.12)', 'hsl(0 62% 32% / 0.2)')};
|
||||||
transform: rotate(360deg);
|
color: ${cssManager.bdTheme('hsl(0 72% 46%)', 'hsl(0 70% 70%)')};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
.remove-button:active {
|
||||||
0% {
|
transform: scale(0.96);
|
||||||
transform: scale(1);
|
}
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.02);
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.uploadButton.loading {
|
.remove-button dees-icon {
|
||||||
animation: pulse 1s ease-in-out infinite;
|
display: block;
|
||||||
}
|
width: 16px;
|
||||||
`,
|
height: 16px;
|
||||||
];
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 72% 40%)', 'hsl(0 70% 68%)')};
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loader-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
@@ -1,84 +0,0 @@
|
|||||||
import { html, type TemplateResult } from '@design.estate/dees-element';
|
|
||||||
import type { DeesInputFileupload } from './component.js';
|
|
||||||
|
|
||||||
export const renderFileupload = (component: DeesInputFileupload): TemplateResult => {
|
|
||||||
const hasFiles = component.value.length > 0;
|
|
||||||
const showClearAll = hasFiles && component.value.length > 1;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="input-wrapper">
|
|
||||||
${component.label ? html`
|
|
||||||
<dees-label .label=${component.label}></dees-label>
|
|
||||||
` : ''}
|
|
||||||
<div class="hidden">
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
?multiple=${component.multiple}
|
|
||||||
accept="${component.accept}"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="maincontainer ${component.state === 'dragOver' ? 'dragOver' : ''}">
|
|
||||||
${hasFiles ? html`
|
|
||||||
${showClearAll ? html`
|
|
||||||
<div class="clear-all-button">
|
|
||||||
<button @click=${component.clearAll}>Clear All</button>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
<div class="files-container">
|
|
||||||
${component.value.map((fileArg) => {
|
|
||||||
const fileType = component.getFileType(fileArg);
|
|
||||||
const isImage = fileType === 'image';
|
|
||||||
return html`
|
|
||||||
<div class="uploadCandidate ${fileType}-file">
|
|
||||||
<div class="icon">
|
|
||||||
${isImage && component.canShowPreview(fileArg) ? html`
|
|
||||||
<img class="image-preview" src="${URL.createObjectURL(fileArg)}" alt="${fileArg.name}">
|
|
||||||
` : html`
|
|
||||||
<dees-icon .icon=${component.getFileIcon(fileArg)}></dees-icon>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
<div class="info">
|
|
||||||
<div class="filename" title="${fileArg.name}">${fileArg.name}</div>
|
|
||||||
<div class="filesize">${component.formatFileSize(fileArg.size)}</div>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<button
|
|
||||||
class="remove-button"
|
|
||||||
@click=${() => component.removeFile(fileArg)}
|
|
||||||
title="Remove file"
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${'lucide:x'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
` : html`
|
|
||||||
<div class="drop-hint">
|
|
||||||
<dees-icon .icon=${'lucide:cloud-upload'}></dees-icon>
|
|
||||||
<div>Drag files here or click to browse</div>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
<div class="uploadButton ${component.isLoading ? 'loading' : ''}" @click=${component.openFileSelector}>
|
|
||||||
<div class="button-content">
|
|
||||||
${component.isLoading ? html`
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
<span>Opening...</span>
|
|
||||||
` : html`
|
|
||||||
<dees-icon .icon=${'lucide:upload'}></dees-icon>
|
|
||||||
${component.buttonText}
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${component.description ? html`
|
|
||||||
<div class="description-text">${component.description}</div>
|
|
||||||
` : ''}
|
|
||||||
${component.validationState === 'invalid' && component.validationMessage ? html`
|
|
||||||
<div class="validation-message">${component.validationMessage}</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
};
|
|
Reference in New Issue
Block a user