Files
dees-catalog/ts_web/elements/dees-input-fileupload.ts

643 lines
18 KiB
TypeScript
Raw Normal View History

2024-01-18 02:08:19 +01:00
import * as colors from './00colors.js';
import * as plugins from './00plugins.js';
import { DeesContextmenu } from './dees-contextmenu.js';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-fileupload.demo.js';
2024-01-18 02:08:19 +01:00
2021-05-05 20:55:49 +00:00
import {
customElement,
DeesElement,
2023-08-07 20:02:18 +02:00
type TemplateResult,
2021-05-05 20:55:49 +00:00
property,
html,
css,
unsafeCSS,
cssManager,
2023-08-07 20:02:18 +02:00
type CSSResult,
2023-10-23 16:13:02 +02:00
domtools,
2023-08-07 19:13:29 +02:00
} from '@design.estate/dees-element';
2021-05-05 20:55:49 +00:00
declare global {
interface HTMLElementTagNameMap {
'dees-input-fileupload': DeesInputFileupload;
}
}
@customElement('dees-input-fileupload')
export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
public static demo = demoFunc;
2021-05-05 20:55:49 +00:00
@property({
attribute: false,
})
public value: File[] = [];
@property()
public state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle';
2022-12-06 13:11:06 +01:00
@property({
type: String,
})
public buttonText: string = 'Upload File...';
2025-06-26 15:32:29 +00:00
@property({ type: String })
public accept: string = '';
@property({ type: Boolean })
public multiple: boolean = true;
@property({ type: Number })
public maxSize: number = 0; // 0 means no limit
@property({ type: Number })
public maxFiles: number = 0; // 0 means no limit
@property({ type: String, reflect: true })
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
2021-08-25 13:51:55 +02:00
constructor() {
2022-12-06 13:11:06 +01:00
super();
2021-08-25 13:51:55 +02:00
}
2021-05-05 20:55:49 +00:00
public static styles = [
...DeesInputBase.baseStyles,
2021-05-05 20:55:49 +00:00
cssManager.defaultStyles,
css`
:host {
position: relative;
2025-06-26 15:32:29 +00:00
display: block;
2024-01-18 02:08:19 +01:00
color: ${cssManager.bdTheme('#333', '#ccc')};
2021-05-05 20:55:49 +00:00
}
2022-12-11 17:24:12 +01:00
.hidden {
display: none;
}
2025-06-26 15:32:29 +00:00
.input-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
}
2021-05-05 20:55:49 +00:00
.maincontainer {
2024-01-18 02:08:19 +01:00
position: relative;
2025-06-26 15:32:29 +00:00
border-radius: 8px;
padding: 16px;
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
2021-05-05 20:55:49 +00:00
color: ${cssManager.bdTheme('#333', '#ccc')};
2025-06-26 15:32:29 +00:00
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
transition: all 0.2s ease;
}
.maincontainer:hover {
border-color: ${cssManager.bdTheme('#ccc', '#444')};
}
:host([disabled]) .maincontainer {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
:host([validationState="invalid"]) .maincontainer {
border-color: #e74c3c;
}
:host([validationState="valid"]) .maincontainer {
border-color: #27ae60;
}
:host([validationState="warn"]) .maincontainer {
border-color: #f39c12;
2024-01-18 02:08:19 +01:00
}
.maincontainer::after {
top: 2px;
right: 2px;
left: 2px;
bottom: 2px;
transform: scale3d(0.98, 0.9, 1);
position: absolute;
content: '';
display: block;
2025-06-26 15:32:29 +00:00
border: 2px dashed transparent;
border-radius: 6px;
transition: all 0.3s ease;
2024-01-18 02:08:19 +01:00
pointer-events: none;
2025-06-26 15:32:29 +00:00
background: transparent;
2024-01-18 02:08:19 +01:00
}
2025-06-26 15:32:29 +00:00
.maincontainer.dragOver {
border-color: ${cssManager.bdTheme('#0084ff', '#0084ff')};
background: ${cssManager.bdTheme('#f0f8ff', '#001933')};
}
2024-01-18 02:08:19 +01:00
.maincontainer.dragOver::after {
transform: scale3d(1, 1, 1);
2025-06-26 15:32:29 +00:00
border: 2px dashed ${cssManager.bdTheme('#0084ff', '#0084ff')};
2021-05-05 20:55:49 +00:00
}
.uploadButton {
position: relative;
2025-06-26 15:32:29 +00:00
padding: 12px 24px;
background: ${cssManager.bdTheme('#0084ff', '#0084ff')};
color: white;
border-radius: 6px;
2021-05-05 20:55:49 +00:00
text-align: center;
2024-01-18 02:08:19 +01:00
font-size: 14px;
2025-06-26 15:32:29 +00:00
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
2021-05-05 20:55:49 +00:00
}
2022-12-06 14:07:12 +01:00
.uploadButton:hover {
2025-06-26 15:32:29 +00:00
background: ${cssManager.bdTheme('#0073e6', '#0073e6')};
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 132, 255, 0.3);
}
.uploadButton:active {
transform: translateY(0);
}
.uploadButton dees-icon {
font-size: 16px;
}
.files-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
2022-12-06 13:11:06 +01:00
}
.uploadCandidate {
2024-01-18 02:08:19 +01:00
display: grid;
2025-06-26 15:32:29 +00:00
grid-template-columns: 40px 1fr auto;
background: ${cssManager.bdTheme('#ffffff', '#2a2a2a')};
padding: 12px;
2022-12-06 13:11:06 +01:00
text-align: left;
2025-06-26 15:32:29 +00:00
border-radius: 6px;
color: ${cssManager.bdTheme('#333', '#ccc')};
2024-01-18 02:08:19 +01:00
cursor: default;
transition: all 0.2s;
2025-06-26 15:32:29 +00:00
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
position: relative;
overflow: hidden;
2022-12-06 14:07:12 +01:00
}
2025-06-26 15:32:29 +00:00
.uploadCandidate:hover {
background: ${cssManager.bdTheme('#f5f5f5', '#333')};
border-color: ${cssManager.bdTheme('#ccc', '#444')};
2021-05-05 20:55:49 +00:00
}
2024-01-18 02:08:19 +01:00
.uploadCandidate .icon {
display: flex;
align-items: center;
justify-content: center;
2025-06-26 15:32:29 +00:00
font-size: 20px;
color: ${cssManager.bdTheme('#666', '#999')};
2024-01-18 02:08:19 +01:00
}
2025-06-26 15:32:29 +00:00
.uploadCandidate.image-file .icon {
color: #4CAF50;
}
.uploadCandidate.pdf-file .icon {
color: #f44336;
}
.uploadCandidate.doc-file .icon {
color: #2196F3;
}
.uploadCandidate .info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.uploadCandidate .filename {
font-weight: 500;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.uploadCandidate .filesize {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
}
.uploadCandidate .actions {
display: flex;
align-items: center;
gap: 8px;
}
.remove-button {
width: 32px;
height: 32px;
border-radius: 4px;
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
color: ${cssManager.bdTheme('#666', '#999')};
}
.remove-button:hover {
background: ${cssManager.bdTheme('#fee', '#4a1c1c')};
color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')};
}
.clear-all-button {
margin-bottom: 8px;
text-align: right;
}
.clear-all-button button {
background: none;
border: none;
color: ${cssManager.bdTheme('#666', '#999')};
cursor: pointer;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
}
.clear-all-button button:hover {
background: ${cssManager.bdTheme('#fee', '#4a1c1c')};
color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')};
}
.validation-message {
font-size: 12px;
margin-top: 4px;
color: #e74c3c;
2024-01-18 02:08:19 +01:00
}
2025-06-26 15:32:29 +00:00
.drop-hint {
text-align: center;
padding: 40px 20px;
color: ${cssManager.bdTheme('#999', '#666')};
2024-01-18 02:08:19 +01:00
font-size: 14px;
2025-06-26 15:32:29 +00:00
}
.drop-hint dees-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.3;
}
.image-preview {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
}
.description-text {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
margin-top: 4px;
line-height: 1.4;
2024-01-18 02:08:19 +01:00
}
2021-05-05 20:55:49 +00:00
`,
];
public render(): TemplateResult {
2025-06-26 15:32:29 +00:00
const hasFiles = this.value.length > 0;
const showClearAll = hasFiles && this.value.length > 1;
2021-05-05 20:55:49 +00:00
return html`
<div class="input-wrapper">
2025-06-26 15:32:29 +00:00
${this.label ? html`
<dees-label .label=${this.label}></dees-label>
` : ''}
<div class="hidden">
2025-06-26 15:32:29 +00:00
<input
type="file"
?multiple=${this.multiple}
accept="${this.accept}"
>
</div>
<div class="maincontainer ${this.state === 'dragOver' ? 'dragOver' : ''}">
2025-06-26 15:32:29 +00:00
${hasFiles ? html`
${showClearAll ? html`
<div class="clear-all-button">
<button @click=${this.clearAll}>Clear All</button>
</div>
` : ''}
<div class="files-container">
${this.value.map((fileArg) => {
const fileType = this.getFileType(fileArg);
const isImage = fileType === 'image';
return html`
<div class="uploadCandidate ${fileType}-file">
<div class="icon">
${isImage && this.canShowPreview(fileArg) ? html`
<img class="image-preview" src="${URL.createObjectURL(fileArg)}" alt="${fileArg.name}">
` : html`
<dees-icon .iconName=${this.getFileIcon(fileArg)}></dees-icon>
`}
</div>
<div class="info">
<div class="filename" title="${fileArg.name}">${fileArg.name}</div>
<div class="filesize">${this.formatFileSize(fileArg.size)}</div>
</div>
<div class="actions">
<button
class="remove-button"
@click=${() => this.removeFile(fileArg)}
title="Remove file"
>
<dees-icon .iconName=${'lucide:x'}></dees-icon>
</button>
</div>
</div>
`;
})}
2024-01-18 02:08:19 +01:00
</div>
2025-06-26 15:32:29 +00:00
` : html`
<div class="drop-hint">
<dees-icon .iconName=${'lucide:cloud-upload'}></dees-icon>
<div>Drag files here or click to browse</div>
2024-01-18 02:08:19 +01:00
</div>
2025-06-26 15:32:29 +00:00
`}
<div class="uploadButton" @click=${this.openFileSelector}>
<dees-icon .iconName=${'lucide:upload'}></dees-icon>
${this.buttonText}
</div>
2022-12-06 13:11:06 +01:00
</div>
2025-06-26 15:32:29 +00:00
${this.description ? html`
<div class="description-text">${this.description}</div>
` : ''}
${this.validationState === 'invalid' && this.validationMessage ? html`
<div class="validation-message">${this.validationMessage}</div>
` : ''}
</div>
2021-05-05 20:55:49 +00:00
`;
}
2025-06-26 15:32:29 +00:00
private validationMessage: string = '';
// Utility methods
private formatFileSize(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
private getFileType(file: File): string {
const type = file.type.toLowerCase();
if (type.startsWith('image/')) return 'image';
if (type === 'application/pdf') return 'pdf';
if (type.includes('word') || type.includes('document')) return 'doc';
if (type.includes('sheet') || type.includes('excel')) return 'spreadsheet';
if (type.includes('presentation') || type.includes('powerpoint')) return 'presentation';
if (type.startsWith('video/')) return 'video';
if (type.startsWith('audio/')) return 'audio';
if (type.includes('zip') || type.includes('compressed')) return 'archive';
return 'file';
}
private getFileIcon(file: File): string {
const type = this.getFileType(file);
const iconMap = {
'image': 'lucide:image',
'pdf': 'lucide:file-text',
'doc': 'lucide:file-text',
'spreadsheet': 'lucide:table',
'presentation': 'lucide:presentation',
'video': 'lucide:video',
'audio': 'lucide:music',
'archive': 'lucide:archive',
'file': 'lucide:file'
};
return iconMap[type] || 'lucide:file';
}
private canShowPreview(file: File): boolean {
return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024; // 5MB limit for previews
}
private validateFile(file: File): boolean {
// Check file size
if (this.maxSize > 0 && file.size > this.maxSize) {
this.validationMessage = `File "${file.name}" exceeds maximum size of ${this.formatFileSize(this.maxSize)}`;
this.validationState = 'invalid';
return false;
}
// Check file type
if (this.accept) {
const acceptedTypes = this.accept.split(',').map(s => s.trim());
let isAccepted = false;
for (const acceptType of acceptedTypes) {
if (acceptType.startsWith('.')) {
// Extension check
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) {
isAccepted = true;
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) {
this.validationMessage = `File type not accepted. Please upload: ${this.accept}`;
this.validationState = 'invalid';
return false;
}
}
return true;
}
2022-12-11 17:24:12 +01:00
public async openFileSelector() {
2025-06-26 15:32:29 +00:00
if (this.disabled) return;
2022-12-11 17:24:12 +01:00
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
inputFile.click();
2025-06-26 15:32:29 +00:00
}
private removeFile(file: File) {
const index = this.value.indexOf(file);
if (index > -1) {
this.value.splice(index, 1);
this.requestUpdate();
this.validate();
this.changeSubject.next(this);
}
}
private clearAll() {
this.value = [];
this.requestUpdate();
this.validate();
this.changeSubject.next(this);
2022-12-11 17:24:12 +01:00
}
2021-05-05 20:55:49 +00:00
public async updateValue(eventArg: Event) {
const target: any = eventArg.target;
this.value = target.value;
2021-08-20 00:25:14 +02:00
this.changeSubject.next(this);
2021-05-05 20:55:49 +00:00
}
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
super.firstUpdated(_changedProperties);
2023-08-19 11:47:45 +02:00
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
2025-06-26 15:32:29 +00:00
inputFile.addEventListener('change', async (event: Event) => {
2023-08-19 11:47:45 +02:00
const target = event.target as HTMLInputElement;
2025-06-26 15:32:29 +00:00
const newFiles = Array.from(target.files);
await this.addFiles(newFiles);
2023-08-19 11:47:45 +02:00
// Reset the input value to allow selecting the same file again if needed
target.value = '';
});
2025-06-26 15:32:29 +00:00
// Handle drag and drop
2024-01-18 02:08:19 +01:00
const dropArea = this.shadowRoot.querySelector('.maincontainer');
2025-06-26 15:32:29 +00:00
const handlerFunction = async (eventArg: DragEvent) => {
2021-05-05 20:55:49 +00:00
eventArg.preventDefault();
2025-06-26 15:32:29 +00:00
eventArg.stopPropagation();
2022-12-06 13:11:06 +01:00
switch (eventArg.type) {
2025-06-26 15:32:29 +00:00
case 'dragenter':
2021-05-05 20:55:49 +00:00
case 'dragover':
this.state = 'dragOver';
break;
case 'dragleave':
2025-06-26 15:32:29 +00:00
// 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';
}
2021-05-05 20:55:49 +00:00
break;
2022-12-06 13:11:06 +01:00
case 'drop':
this.state = 'idle';
2025-06-26 15:32:29 +00:00
const files = Array.from(eventArg.dataTransfer.files);
await this.addFiles(files);
break;
2022-12-06 13:11:06 +01:00
}
2021-05-05 20:55:49 +00:00
};
2025-06-26 15:32:29 +00:00
2021-05-05 20:55:49 +00:00
dropArea.addEventListener('dragenter', handlerFunction, false);
dropArea.addEventListener('dragleave', handlerFunction, false);
dropArea.addEventListener('dragover', handlerFunction, false);
dropArea.addEventListener('drop', handlerFunction, false);
}
2025-06-26 15:32:29 +00:00
private async addFiles(files: File[]) {
const filesToAdd: File[] = [];
for (const file of files) {
if (this.validateFile(file)) {
filesToAdd.push(file);
}
}
if (filesToAdd.length === 0) return;
// Check max files limit
if (this.maxFiles > 0) {
const totalFiles = this.value.length + filesToAdd.length;
if (totalFiles > this.maxFiles) {
const allowedCount = this.maxFiles - this.value.length;
if (allowedCount <= 0) {
this.validationMessage = `Maximum ${this.maxFiles} files allowed`;
this.validationState = 'invalid';
return;
}
filesToAdd.splice(allowedCount);
this.validationMessage = `Only ${allowedCount} more file(s) can be added`;
this.validationState = 'warn';
}
}
// Add files
if (!this.multiple && filesToAdd.length > 0) {
this.value = [filesToAdd[0]];
} else {
this.value.push(...filesToAdd);
}
this.requestUpdate();
this.validate();
this.changeSubject.next(this);
// Update button text
if (this.value.length > 0) {
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
}
}
public async validate(): Promise<boolean> {
this.validationMessage = '';
if (this.required && this.value.length === 0) {
this.validationState = 'invalid';
this.validationMessage = 'Please select at least one file';
return false;
}
// Validate all files
for (const file of this.value) {
if (!this.validateFile(file)) {
return false;
}
}
this.validationState = 'valid';
return true;
}
public getValue(): File[] {
return this.value;
}
public setValue(value: File[]): void {
this.value = value;
2025-06-26 15:32:29 +00:00
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();
}
}
2021-05-05 20:55:49 +00:00
}