implement image upload

This commit is contained in:
Juergen Kunz
2025-06-24 17:16:13 +00:00
parent 90fc8bed35
commit fca3638f7f
5 changed files with 321 additions and 13 deletions

View File

@ -259,6 +259,92 @@ export class DeesWysiwygBlock extends DeesElement {
padding-left: 8px;
padding-right: 8px;
}
/* Image block styles */
.block.image {
min-height: 200px;
padding: 0;
margin: 16px 0;
border-radius: 8px;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.image-upload-placeholder {
width: 100%;
height: 200px;
background: ${cssManager.bdTheme('#f8f8f8', '#1a1a1a')};
border: 2px dashed ${cssManager.bdTheme('#d0d0d0', '#404040')};
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.image-upload-placeholder:hover {
background: ${cssManager.bdTheme('#f0f0f0', '#222222')};
border-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
}
.image-upload-placeholder:active {
transform: scale(0.98);
}
.image-upload-placeholder.drag-over {
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
border-color: ${cssManager.bdTheme('#2196F3', '#64b5f6')};
}
.upload-icon {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.7;
}
.upload-text {
font-size: 16px;
color: ${cssManager.bdTheme('#666', '#999')};
margin-bottom: 8px;
}
.upload-hint {
font-size: 13px;
color: ${cssManager.bdTheme('#999', '#666')};
}
.image-container {
width: 100%;
position: relative;
}
.image-container img {
width: 100%;
height: auto;
display: block;
border-radius: 8px;
}
.image-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 16px 24px;
background: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 8px;
font-size: 14px;
}
input[type="file"] {
display: none;
}
`,
];
@ -286,6 +372,12 @@ export class DeesWysiwygBlock extends DeesElement {
container.innerHTML = this.renderBlockContent();
}
// Handle image block setup
if (this.block.type === 'image') {
this.setupImageBlock();
return; // Image blocks don't need the standard editable setup
}
// Now find the actual editable block element
const editableBlock = this.block.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
@ -505,6 +597,32 @@ export class DeesWysiwygBlock extends DeesElement {
`;
}
if (this.block.type === 'image') {
const selectedClass = this.isSelected ? ' selected' : '';
const imageUrl = this.block.metadata?.url || '';
const isLoading = this.block.metadata?.loading || false;
return `
<div class="block image${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}">
${isLoading ? `
<div class="image-loading">Uploading image...</div>
` : ''}
${imageUrl ? `
<div class="image-container">
<img src="${imageUrl}" alt="${this.block.content || 'Uploaded image'}" />
</div>
` : `
<div class="image-upload-placeholder">
<div class="upload-icon">🖼️</div>
<div class="upload-text">Click to upload an image</div>
<div class="upload-hint">or drag and drop</div>
</div>
<input type="file" accept="image/*" style="display: none;" />
`}
</div>
`;
}
const placeholder = this.getPlaceholder();
const selectedClass = this.isSelected ? ' selected' : '';
return `
@ -528,6 +646,8 @@ export class DeesWysiwygBlock extends DeesElement {
return 'Heading 3';
case 'quote':
return 'Quote';
case 'image':
return 'Click to upload an image';
default:
return '';
}
@ -535,6 +655,15 @@ export class DeesWysiwygBlock extends DeesElement {
public focus(): void {
// Image blocks don't focus in the traditional way
if (this.block?.type === 'image') {
const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement;
if (imageBlock) {
imageBlock.focus();
}
return;
}
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
@ -558,6 +687,12 @@ export class DeesWysiwygBlock extends DeesElement {
}
public focusWithCursor(position: 'start' | 'end' | number = 'end'): void {
// Image blocks don't support cursor positioning
if (this.block?.type === 'image') {
this.focus();
return;
}
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
@ -654,6 +789,11 @@ export class DeesWysiwygBlock extends DeesElement {
}
public getContent(): string {
// Handle image blocks specially
if (this.block?.type === 'image') {
return this.block.content || ''; // Image blocks store alt text in content
}
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
@ -726,12 +866,153 @@ export class DeesWysiwygBlock extends DeesElement {
}
}
/**
* Setup image block functionality
*/
private setupImageBlock(): void {
const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement;
if (!imageBlock) return;
// Make the image block focusable
imageBlock.setAttribute('tabindex', '0');
// Handle click on upload placeholder
const uploadPlaceholder = imageBlock.querySelector('.image-upload-placeholder');
const fileInput = imageBlock.querySelector('input[type="file"]') as HTMLInputElement;
if (uploadPlaceholder && fileInput) {
uploadPlaceholder.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
this.handleImageUpload(file);
}
});
// Handle drag and drop
imageBlock.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
uploadPlaceholder.classList.add('drag-over');
});
imageBlock.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
uploadPlaceholder.classList.remove('drag-over');
});
imageBlock.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
uploadPlaceholder.classList.remove('drag-over');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const file = files[0];
if (file.type.startsWith('image/')) {
this.handleImageUpload(file);
}
}
});
}
// Handle focus/blur for the image block
imageBlock.addEventListener('focus', () => {
this.handlers?.onFocus?.();
});
imageBlock.addEventListener('blur', () => {
this.handlers?.onBlur?.();
});
// Handle keyboard events
imageBlock.addEventListener('keydown', (e) => {
this.handlers?.onKeyDown?.(e);
});
}
/**
* Handle image file upload
*/
private async handleImageUpload(file: File): Promise<void> {
// Check file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
alert('Image size must be less than 10MB');
return;
}
// Update block to show loading state
this.block.metadata = { ...this.block.metadata, loading: true };
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
if (container) {
container.innerHTML = this.renderBlockContent();
this.setupImageBlock(); // Re-setup event handlers
}
try {
// Convert to base64 for now (in production, you'd upload to a server)
const reader = new FileReader();
reader.onload = (e) => {
const base64 = e.target?.result as string;
// Update block with image URL
this.block.metadata = {
...this.block.metadata,
url: base64,
loading: false,
fileName: file.name,
fileSize: file.size,
mimeType: file.type
};
// Set alt text as content
this.block.content = file.name.replace(/\.[^/.]+$/, ''); // Remove extension
// Re-render
if (container) {
container.innerHTML = this.renderBlockContent();
}
// Notify parent component of the change
this.handlers?.onInput?.(new InputEvent('input'));
};
reader.onerror = () => {
alert('Failed to read image file');
this.block.metadata = { ...this.block.metadata, loading: false };
if (container) {
container.innerHTML = this.renderBlockContent();
this.setupImageBlock();
}
};
reader.readAsDataURL(file);
} catch (error) {
console.error('Error uploading image:', error);
alert('Failed to upload image');
this.block.metadata = { ...this.block.metadata, loading: false };
if (container) {
container.innerHTML = this.renderBlockContent();
this.setupImageBlock();
}
}
}
/**
* Gets content split at cursor position
*/
public getSplitContent(): { before: string; after: string } | null {
console.log('getSplitContent: Starting...');
// Image blocks can't be split
if (this.block?.type === 'image') {
return null;
}
// Get the actual editable element first
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement