implement image upload
This commit is contained in:
@ -533,6 +533,10 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
currentBlock.metadata = { listType: 'bullet' };
|
||||
// For lists, ensure we start with empty content
|
||||
currentBlock.content = '';
|
||||
} else if (type === 'image') {
|
||||
// For image blocks, clear content and set empty metadata
|
||||
currentBlock.content = '';
|
||||
currentBlock.metadata = { url: '', loading: false };
|
||||
} else {
|
||||
// For all other block types, ensure content is clean
|
||||
currentBlock.content = currentBlock.content || '';
|
||||
@ -556,8 +560,11 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
blockComponent.focusListItem();
|
||||
}
|
||||
});
|
||||
} else if (type !== 'divider') {
|
||||
} else if (type !== 'divider' && type !== 'image') {
|
||||
this.blockOperations.focusBlock(currentBlock.id, 'start');
|
||||
} else if (type === 'image') {
|
||||
// Focus the image block (which will show the upload interface)
|
||||
this.blockOperations.focusBlock(currentBlock.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -37,6 +37,13 @@ export class WysiwygConverters {
|
||||
return '';
|
||||
case 'divider':
|
||||
return '<hr>';
|
||||
case 'image':
|
||||
const imageUrl = block.metadata?.url;
|
||||
if (imageUrl) {
|
||||
const altText = this.escapeHtml(block.content || 'Image');
|
||||
return `<img src="${imageUrl}" alt="${altText}" />`;
|
||||
}
|
||||
return '';
|
||||
default:
|
||||
return `<p>${content}</p>`;
|
||||
}
|
||||
@ -67,6 +74,10 @@ export class WysiwygConverters {
|
||||
}
|
||||
case 'divider':
|
||||
return '---';
|
||||
case 'image':
|
||||
const imageUrl = block.metadata?.url;
|
||||
const altText = block.content || 'Image';
|
||||
return imageUrl ? `` : '';
|
||||
default:
|
||||
return block.content;
|
||||
}
|
||||
@ -152,6 +163,15 @@ export class WysiwygConverters {
|
||||
content: ' ',
|
||||
});
|
||||
break;
|
||||
case 'img':
|
||||
const imgElement = element as HTMLImageElement;
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'image',
|
||||
content: imgElement.alt || '',
|
||||
metadata: { url: imgElement.src }
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// Process children for other elements
|
||||
element.childNodes.forEach(child => processNode(child));
|
||||
@ -237,6 +257,17 @@ export class WysiwygConverters {
|
||||
type: 'divider',
|
||||
content: ' ',
|
||||
});
|
||||
} else if (line.match(/^!\[([^\]]*)\]\(([^\)]+)\)$/)) {
|
||||
// Parse markdown image syntax 
|
||||
const match = line.match(/^!\[([^\]]*)\]\(([^\)]+)\)$/);
|
||||
if (match) {
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: 'image',
|
||||
content: match[1] || '',
|
||||
metadata: { url: match[2] }
|
||||
});
|
||||
}
|
||||
} else if (line.trim()) {
|
||||
blocks.push({
|
||||
id: `block-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
|
@ -114,14 +114,11 @@ export class WysiwygFormatting {
|
||||
|
||||
// Check if ANY part of the selection contains this formatting
|
||||
const hasFormatting = this.selectionContainsTag(range, tagName);
|
||||
console.log(`Formatting check for <${tagName}>: ${hasFormatting ? 'HAS formatting' : 'NO formatting'}`);
|
||||
|
||||
if (hasFormatting) {
|
||||
console.log(`Removing <${tagName}> formatting from selection`);
|
||||
// Remove all instances of this tag from the selection
|
||||
this.removeTagFromSelection(range, tagName);
|
||||
} else {
|
||||
console.log(`Adding <${tagName}> formatting to selection`);
|
||||
// Wrap selection with the tag
|
||||
const wrapper = document.createElement(tagName);
|
||||
try {
|
||||
@ -144,18 +141,13 @@ export class WysiwygFormatting {
|
||||
* Check if the selection contains or is within any instances of a tag
|
||||
*/
|
||||
private static selectionContainsTag(range: Range, tagName: string): boolean {
|
||||
console.log(`Checking if selection contains <${tagName}>...`);
|
||||
|
||||
// First check: Are we inside a tag? (even if selection doesn't include the tag)
|
||||
let node: Node | null = range.startContainer;
|
||||
console.log('Start container:', range.startContainer, 'type:', range.startContainer.nodeType);
|
||||
|
||||
while (node && node !== range.commonAncestorContainer.ownerDocument) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element;
|
||||
console.log(` Checking parent element: <${element.tagName.toLowerCase()}>`);
|
||||
if (element.tagName.toLowerCase() === tagName) {
|
||||
console.log(` ✓ Found <${tagName}> as parent of start container`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -164,14 +156,11 @@ export class WysiwygFormatting {
|
||||
|
||||
// Also check the end container
|
||||
node = range.endContainer;
|
||||
console.log('End container:', range.endContainer, 'type:', range.endContainer.nodeType);
|
||||
|
||||
while (node && node !== range.commonAncestorContainer.ownerDocument) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element;
|
||||
console.log(` Checking parent element: <${element.tagName.toLowerCase()}>`);
|
||||
if (element.tagName.toLowerCase() === tagName) {
|
||||
console.log(` ✓ Found <${tagName}> as parent of end container`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -183,7 +172,6 @@ export class WysiwygFormatting {
|
||||
const contents = range.cloneContents();
|
||||
tempDiv.appendChild(contents);
|
||||
const tags = tempDiv.getElementsByTagName(tagName);
|
||||
console.log(` Selection contains ${tags.length} complete <${tagName}> tags`);
|
||||
|
||||
return tags.length > 0;
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ export class WysiwygShortcuts {
|
||||
{ type: 'quote', label: 'Quote', icon: '"' },
|
||||
{ type: 'code', label: 'Code', icon: '<>' },
|
||||
{ type: 'list', label: 'List', icon: '•' },
|
||||
{ type: 'image', label: 'Image', icon: '🖼' },
|
||||
{ type: 'divider', label: 'Divider', icon: '—' },
|
||||
];
|
||||
}
|
||||
|
Reference in New Issue
Block a user