feat(wysiwyg): Add more block types

This commit is contained in:
Juergen Kunz
2025-06-24 20:32:03 +00:00
parent 856d354b5a
commit 68b4e9ec8e
6 changed files with 998 additions and 54 deletions

View File

@ -368,12 +368,345 @@ export class DeesWysiwygBlock extends DeesElement {
input[type="file"] {
display: none;
}
/* YouTube block styles */
.block.youtube {
padding: 0;
margin: 16px 0;
border-radius: 8px;
overflow: hidden;
position: relative;
cursor: pointer;
transition: all 0.15s ease;
}
.block.youtube:focus {
outline: none;
}
.block.youtube.selected {
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')};
}
.youtube-container {
position: relative;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
height: 0;
overflow: hidden;
}
.youtube-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
.youtube-placeholder {
background: ${cssManager.bdTheme('#f8f8f8', '#1a1a1a')};
border: 2px dashed ${cssManager.bdTheme('#d0d0d0', '#404040')};
border-radius: 8px;
padding: 40px;
text-align: center;
}
.placeholder-icon {
font-size: 48px;
margin-bottom: 16px;
}
.placeholder-text {
font-size: 16px;
color: ${cssManager.bdTheme('#666', '#999')};
margin-bottom: 16px;
}
.youtube-url-input {
width: 100%;
max-width: 400px;
padding: 12px;
border: 1px solid ${cssManager.bdTheme('#ddd', '#444')};
border-radius: 6px;
font-size: 14px;
margin-bottom: 16px;
background: ${cssManager.bdTheme('#fff', '#222')};
color: ${cssManager.bdTheme('#000', '#fff')};
}
.youtube-embed-btn {
padding: 10px 24px;
background: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
}
.youtube-embed-btn:hover {
background: ${cssManager.bdTheme('#0052a3', '#3d7dd9')};
}
/* Markdown block styles */
.block.markdown {
padding: 0;
margin: 16px 0;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.15s ease;
}
.block.markdown:focus {
outline: none;
}
.block.markdown.selected {
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')};
}
.markdown-toolbar,
.html-toolbar {
background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')};
padding: 8px 16px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
}
.markdown-label,
.html-label {
font-size: 12px;
text-transform: uppercase;
color: ${cssManager.bdTheme('#666', '#999')};
font-weight: 500;
}
.toggle-preview {
padding: 6px 12px;
background: ${cssManager.bdTheme('#fff', '#333')};
border: 1px solid ${cssManager.bdTheme('#ddd', '#555')};
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.toggle-preview:hover {
background: ${cssManager.bdTheme('#f0f0f0', '#444')};
}
.markdown-content,
.html-content {
min-height: 200px;
}
.markdown-editor,
.html-editor {
width: 100%;
min-height: 200px;
padding: 16px;
border: none;
resize: vertical;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
font-size: 14px;
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
color: ${cssManager.bdTheme('#000', '#fff')};
}
.markdown-preview,
.html-preview {
padding: 16px;
min-height: 200px;
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
}
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3 {
margin-top: 16px;
margin-bottom: 8px;
}
.markdown-preview h1:first-child,
.markdown-preview h2:first-child,
.markdown-preview h3:first-child {
margin-top: 0;
}
/* HTML block styles */
.block.html {
padding: 0;
margin: 16px 0;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.15s ease;
}
.block.html:focus {
outline: none;
}
.block.html.selected {
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')};
}
/* Attachment block styles */
.block.attachment {
padding: 0;
margin: 16px 0;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.15s ease;
}
.block.attachment:focus {
outline: none;
}
.block.attachment.selected {
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')};
}
.block.attachment.drag-over {
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
}
.attachment-header {
background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')};
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
}
.attachment-icon {
font-size: 24px;
}
.attachment-title {
font-size: 16px;
font-weight: 500;
}
.attachment-list {
padding: 16px;
min-height: 100px;
}
.attachment-placeholder {
text-align: center;
padding: 40px;
border: 2px dashed ${cssManager.bdTheme('#d0d0d0', '#404040')};
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.attachment-placeholder:hover {
background: ${cssManager.bdTheme('#f8f8f8', '#1a1a1a')};
border-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
}
.placeholder-hint {
font-size: 13px;
color: ${cssManager.bdTheme('#999', '#666')};
margin-top: 8px;
}
.attachment-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: ${cssManager.bdTheme('#f8f8f8', '#222')};
border-radius: 6px;
margin-bottom: 8px;
transition: background 0.2s ease;
}
.attachment-item:hover {
background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')};
}
.file-icon {
font-size: 24px;
flex-shrink: 0;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-size {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
}
.remove-file {
width: 24px;
height: 24px;
border: none;
background: transparent;
color: ${cssManager.bdTheme('#999', '#666')};
font-size: 20px;
line-height: 1;
cursor: pointer;
opacity: 0;
transition: all 0.2s ease;
}
.attachment-item:hover .remove-file {
opacity: 1;
}
.remove-file:hover {
color: ${cssManager.bdTheme('#d32f2f', '#f44336')};
}
.add-more-files {
width: 100%;
padding: 10px;
background: transparent;
border: 1px dashed ${cssManager.bdTheme('#ddd', '#444')};
border-radius: 6px;
color: ${cssManager.bdTheme('#666', '#999')};
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.add-more-files:hover {
background: ${cssManager.bdTheme('#f8f8f8', '#1a1a1a')};
border-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
}
`,
];
protected shouldUpdate(changedProperties: Map<string, any>): boolean {
// If selection state changed, we need to update for non-editable blocks
if (changedProperties.has('isSelected') && (this.block?.type === 'divider' || this.block?.type === 'image')) {
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
if (changedProperties.has('isSelected') && this.block && nonEditableTypes.includes(this.block.type)) {
// For non-editable blocks, we need to update the selected class
const element = this.shadowRoot?.querySelector('.block') as HTMLElement;
if (element) {
@ -416,6 +749,18 @@ export class DeesWysiwygBlock extends DeesElement {
} else if (this.block.type === 'divider') {
this.setupDividerBlock();
return; // Divider blocks don't need the standard editable setup
} else if (this.block.type === 'youtube') {
this.setupYouTubeBlock();
return;
} else if (this.block.type === 'markdown') {
this.setupMarkdownBlock();
return;
} else if (this.block.type === 'html') {
this.setupHtmlBlock();
return;
} else if (this.block.type === 'attachment') {
this.setupAttachmentBlock();
return;
}
// Now find the actual editable block element
@ -470,7 +815,6 @@ export class DeesWysiwygBlock extends DeesElement {
const pos = this.getCursorPosition(editableBlock);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
console.log('Cursor position after mouseup:', pos);
}
// Selection will be handled by selectionchange event
@ -483,7 +827,6 @@ export class DeesWysiwygBlock extends DeesElement {
const pos = this.getCursorPosition(editableBlock);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
console.log('Cursor position after click:', pos);
}
}, 0);
});
@ -538,7 +881,6 @@ export class DeesWysiwygBlock extends DeesElement {
if (startInBlock || endInBlock) {
if (selectedText !== this.lastSelectedText) {
this.lastSelectedText = selectedText;
console.log('✅ Selection detected in block using getComposedRanges:', selectedText);
// Create range and get rect
const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
@ -664,6 +1006,113 @@ export class DeesWysiwygBlock extends DeesElement {
`;
}
if (this.block.type === 'youtube') {
const selectedClass = this.isSelected ? ' selected' : '';
const videoId = this.block.metadata?.videoId || '';
const url = this.block.metadata?.url || '';
return `
<div class="block youtube${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}" tabindex="0">
${videoId ? `
<div class="youtube-container">
<iframe
src="https://www.youtube.com/embed/${videoId}"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
` : `
<div class="youtube-placeholder">
<div class="placeholder-icon">▶️</div>
<div class="placeholder-text">Enter YouTube URL</div>
<input type="url" class="youtube-url-input" placeholder="https://youtube.com/watch?v=..." value="${url || ''}" />
<button class="youtube-embed-btn">Embed Video</button>
</div>
`}
</div>
`;
}
if (this.block.type === 'markdown') {
const selectedClass = this.isSelected ? ' selected' : '';
const showPreview = this.block.metadata?.showPreview !== false;
return `
<div class="block markdown${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}" tabindex="0">
<div class="markdown-toolbar">
<button class="toggle-preview" data-active="${showPreview}">
${showPreview ? 'Edit' : 'Preview'}
</button>
<span class="markdown-label">Markdown</span>
</div>
<div class="markdown-content">
${showPreview ? `
<div class="markdown-preview"></div>
` : `
<textarea class="markdown-editor" placeholder="Write markdown here...">${this.block.content || ''}</textarea>
`}
</div>
</div>
`;
}
if (this.block.type === 'html') {
const selectedClass = this.isSelected ? ' selected' : '';
const showPreview = this.block.metadata?.showPreview !== false;
return `
<div class="block html${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}" tabindex="0">
<div class="html-toolbar">
<button class="toggle-preview" data-active="${showPreview}">
${showPreview ? 'Edit' : 'Preview'}
</button>
<span class="html-label">HTML</span>
</div>
<div class="html-content">
${showPreview ? `
<div class="html-preview"></div>
` : `
<textarea class="html-editor" placeholder="Write HTML here...">${this.block.content || ''}</textarea>
`}
</div>
</div>
`;
}
if (this.block.type === 'attachment') {
const selectedClass = this.isSelected ? ' selected' : '';
const files = this.block.metadata?.files || [];
return `
<div class="block attachment${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}" tabindex="0">
<div class="attachment-header">
<div class="attachment-icon">📎</div>
<div class="attachment-title">File Attachments</div>
</div>
<div class="attachment-list">
${files.length > 0 ? files.map((file: any) => `
<div class="attachment-item">
<div class="file-icon">${this.getFileIcon(file.type)}</div>
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-size">${this.formatFileSize(file.size)}</div>
</div>
<button class="remove-file" data-file-id="${file.id}">×</button>
</div>
`).join('') : `
<div class="attachment-placeholder">
<div class="placeholder-text">Click to add files</div>
<div class="placeholder-hint">or drag and drop</div>
</div>
`}
</div>
<input type="file" multiple style="display: none;" />
${files.length > 0 ? '<button class="add-more-files">Add More Files</button>' : ''}
</div>
`;
}
const placeholder = this.getPlaceholder();
const selectedClass = this.isSelected ? ' selected' : '';
return `
@ -697,16 +1146,11 @@ export class DeesWysiwygBlock extends DeesElement {
public focus(): void {
// Handle non-editable blocks
if (this.block?.type === 'image') {
const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement;
if (imageBlock) {
imageBlock.focus();
}
return;
} else if (this.block?.type === 'divider') {
const dividerBlock = this.shadowRoot?.querySelector('.block.divider') as HTMLDivElement;
if (dividerBlock) {
dividerBlock.focus();
const nonEditableTypes = ['image', 'divider', 'youtube', 'markdown', 'html', 'attachment'];
if (this.block && nonEditableTypes.includes(this.block.type)) {
const blockElement = this.shadowRoot?.querySelector(`.block.${this.block.type}`) as HTMLDivElement;
if (blockElement) {
blockElement.focus();
}
return;
}
@ -735,7 +1179,8 @@ export class DeesWysiwygBlock extends DeesElement {
public focusWithCursor(position: 'start' | 'end' | number = 'end'): void {
// Non-editable blocks don't support cursor positioning
if (this.block?.type === 'image' || this.block?.type === 'divider') {
const nonEditableTypes = ['image', 'divider', 'youtube', 'markdown', 'html', 'attachment'];
if (this.block && nonEditableTypes.includes(this.block.type)) {
this.focus();
return;
}
@ -950,7 +1395,439 @@ export class DeesWysiwygBlock extends DeesElement {
}
});
}
/**
* Setup YouTube block functionality
*/
private setupYouTubeBlock(): void {
const youtubeBlock = this.shadowRoot?.querySelector('.block.youtube') as HTMLDivElement;
if (!youtubeBlock) return;
// Handle click to select
youtubeBlock.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (!target.classList.contains('youtube-url-input') && !target.classList.contains('youtube-embed-btn')) {
e.stopPropagation();
youtubeBlock.focus();
this.handlers?.onFocus?.();
}
});
// Handle URL input and embed button
const urlInput = youtubeBlock.querySelector('.youtube-url-input') as HTMLInputElement;
const embedBtn = youtubeBlock.querySelector('.youtube-embed-btn') as HTMLButtonElement;
if (urlInput && embedBtn) {
const embedVideo = () => {
const url = urlInput.value.trim();
if (url) {
// Extract video ID from YouTube URL
const videoId = this.extractYouTubeVideoId(url);
if (videoId) {
this.block.metadata = { ...this.block.metadata, videoId, url };
this.block.content = url; // Store URL as content
// Re-render the block
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
if (container) {
container.innerHTML = this.renderBlockContent();
this.setupYouTubeBlock(); // Re-setup event handlers
}
// Notify parent of change
this.handlers?.onInput?.(new InputEvent('input'));
} else {
alert('Invalid YouTube URL');
}
}
};
embedBtn.addEventListener('click', embedVideo);
urlInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
embedVideo();
}
});
}
// Handle focus/blur
youtubeBlock.addEventListener('focus', () => this.handlers?.onFocus?.());
youtubeBlock.addEventListener('blur', () => this.handlers?.onBlur?.());
// Handle keyboard events
youtubeBlock.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
e.preventDefault();
this.handlers?.onKeyDown?.(e);
} else {
this.handlers?.onKeyDown?.(e);
}
});
}
/**
* Setup Markdown block functionality
*/
private setupMarkdownBlock(): void {
const markdownBlock = this.shadowRoot?.querySelector('.block.markdown') as HTMLDivElement;
if (!markdownBlock) return;
// Handle click to select
markdownBlock.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (!target.classList.contains('markdown-editor') && !target.classList.contains('toggle-preview')) {
e.stopPropagation();
markdownBlock.focus();
this.handlers?.onFocus?.();
}
});
// Handle preview toggle
const toggleBtn = markdownBlock.querySelector('.toggle-preview') as HTMLButtonElement;
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
const showPreview = toggleBtn.dataset.active !== 'true';
this.block.metadata = { ...this.block.metadata, showPreview };
// Re-render
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
if (container) {
container.innerHTML = this.renderBlockContent();
this.setupMarkdownBlock();
// If switching to preview, render markdown
if (showPreview) {
this.renderMarkdownPreview();
}
}
});
}
// Handle editor input
const editor = markdownBlock.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
editor.addEventListener('input', () => {
this.block.content = editor.value;
this.handlers?.onInput?.(new InputEvent('input'));
});
// Auto-resize textarea
const autoResize = () => {
editor.style.height = 'auto';
editor.style.height = editor.scrollHeight + 'px';
};
editor.addEventListener('input', autoResize);
autoResize();
}
// Render preview if needed
if (this.block.metadata?.showPreview) {
this.renderMarkdownPreview();
}
// Handle focus/blur
markdownBlock.addEventListener('focus', () => this.handlers?.onFocus?.());
markdownBlock.addEventListener('blur', () => this.handlers?.onBlur?.());
// Handle keyboard events
markdownBlock.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
e.preventDefault();
this.handlers?.onKeyDown?.(e);
} else {
this.handlers?.onKeyDown?.(e);
}
});
}
/**
* Setup HTML block functionality
*/
private setupHtmlBlock(): void {
const htmlBlock = this.shadowRoot?.querySelector('.block.html') as HTMLDivElement;
if (!htmlBlock) return;
// Handle click to select
htmlBlock.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (!target.classList.contains('html-editor') && !target.classList.contains('toggle-preview')) {
e.stopPropagation();
htmlBlock.focus();
this.handlers?.onFocus?.();
}
});
// Handle preview toggle
const toggleBtn = htmlBlock.querySelector('.toggle-preview') as HTMLButtonElement;
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
const showPreview = toggleBtn.dataset.active !== 'true';
this.block.metadata = { ...this.block.metadata, showPreview };
// Re-render
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
if (container) {
container.innerHTML = this.renderBlockContent();
this.setupHtmlBlock();
// If switching to preview, render HTML
if (showPreview) {
this.renderHtmlPreview();
}
}
});
}
// Handle editor input
const editor = htmlBlock.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
editor.addEventListener('input', () => {
this.block.content = editor.value;
this.handlers?.onInput?.(new InputEvent('input'));
});
// Auto-resize textarea
const autoResize = () => {
editor.style.height = 'auto';
editor.style.height = editor.scrollHeight + 'px';
};
editor.addEventListener('input', autoResize);
autoResize();
}
// Render preview if needed
if (this.block.metadata?.showPreview) {
this.renderHtmlPreview();
}
// Handle focus/blur
htmlBlock.addEventListener('focus', () => this.handlers?.onFocus?.());
htmlBlock.addEventListener('blur', () => this.handlers?.onBlur?.());
// Handle keyboard events
htmlBlock.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
e.preventDefault();
this.handlers?.onKeyDown?.(e);
} else {
this.handlers?.onKeyDown?.(e);
}
});
}
/**
* Setup Attachment block functionality
*/
private setupAttachmentBlock(): void {
const attachmentBlock = this.shadowRoot?.querySelector('.block.attachment') as HTMLDivElement;
if (!attachmentBlock) return;
// Handle click to select
attachmentBlock.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (!target.classList.contains('remove-file')) {
e.stopPropagation();
attachmentBlock.focus();
this.handlers?.onFocus?.();
}
});
// Handle file input
const fileInput = attachmentBlock.querySelector('input[type="file"]') as HTMLInputElement;
const placeholder = attachmentBlock.querySelector('.attachment-placeholder');
const addMoreBtn = attachmentBlock.querySelector('.add-more-files') as HTMLButtonElement;
const triggerFileInput = () => {
if (fileInput) fileInput.click();
};
if (placeholder) {
placeholder.addEventListener('click', triggerFileInput);
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', triggerFileInput);
}
if (fileInput) {
fileInput.addEventListener('change', async (e) => {
const files = Array.from((e.target as HTMLInputElement).files || []);
if (files.length > 0) {
await this.handleFileAttachments(files);
}
});
}
// Handle file removal
attachmentBlock.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.classList.contains('remove-file')) {
const fileId = target.dataset.fileId;
if (fileId) {
const files = this.block.metadata?.files || [];
this.block.metadata = {
...this.block.metadata,
files: files.filter((f: any) => f.id !== fileId)
};
// Re-render
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
if (container) {
container.innerHTML = this.renderBlockContent();
this.setupAttachmentBlock();
}
this.handlers?.onInput?.(new InputEvent('input'));
}
}
});
// Handle drag and drop
attachmentBlock.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
attachmentBlock.classList.add('drag-over');
});
attachmentBlock.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
attachmentBlock.classList.remove('drag-over');
});
attachmentBlock.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
attachmentBlock.classList.remove('drag-over');
const files = Array.from(e.dataTransfer?.files || []);
if (files.length > 0) {
await this.handleFileAttachments(files);
}
});
// Handle focus/blur
attachmentBlock.addEventListener('focus', () => this.handlers?.onFocus?.());
attachmentBlock.addEventListener('blur', () => this.handlers?.onBlur?.());
// Handle keyboard events
attachmentBlock.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
e.preventDefault();
this.handlers?.onKeyDown?.(e);
} else {
this.handlers?.onKeyDown?.(e);
}
});
}
/**
* Extract YouTube video ID from URL
*/
private extractYouTubeVideoId(url: string): string | null {
const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/;
const match = url.match(regex);
return match ? match[1] : null;
}
/**
* Render Markdown preview
*/
private async renderMarkdownPreview(): Promise<void> {
const preview = this.shadowRoot?.querySelector('.markdown-preview') as HTMLDivElement;
if (!preview || !this.block.content) return;
// Simple markdown to HTML conversion (you might want to use a proper markdown parser)
let html = this.block.content
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/\*\*(.*)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*)\*/g, '<em>$1</em>')
.replace(/\[([^\]]*)\]\(([^\)]*)\)/g, '<a href="$2">$1</a>')
.replace(/\n/g, '<br>');
preview.innerHTML = html;
}
/**
* Render HTML preview
*/
private renderHtmlPreview(): void {
const preview = this.shadowRoot?.querySelector('.html-preview') as HTMLDivElement;
if (!preview || !this.block.content) return;
// Render HTML in a sandboxed way
preview.innerHTML = this.block.content;
}
/**
* Handle file attachments
*/
private async handleFileAttachments(files: File[]): Promise<void> {
const existingFiles = this.block.metadata?.files || [];
const newFiles: any[] = [];
for (const file of files) {
// Convert to base64 for storage (in production, upload to server)
const reader = new FileReader();
const base64 = await new Promise<string>((resolve) => {
reader.onload = (e) => resolve(e.target?.result as string);
reader.readAsDataURL(file);
});
newFiles.push({
id: `file-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
name: file.name,
size: file.size,
type: file.type,
data: base64
});
}
this.block.metadata = {
...this.block.metadata,
files: [...existingFiles, ...newFiles]
};
// Re-render
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
if (container) {
container.innerHTML = this.renderBlockContent();
this.setupAttachmentBlock();
}
this.handlers?.onInput?.(new InputEvent('input'));
}
/**
* Get file icon based on mime type
*/
private getFileIcon(mimeType: string): string {
if (mimeType.startsWith('image/')) return '🖼️';
if (mimeType.startsWith('video/')) return '🎥';
if (mimeType.startsWith('audio/')) return '🎵';
if (mimeType.includes('pdf')) return '📄';
if (mimeType.includes('zip') || mimeType.includes('compressed')) return '🗄️';
if (mimeType.includes('sheet') || mimeType.includes('excel')) return '📊';
if (mimeType.includes('document') || mimeType.includes('word')) return '📝';
if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) return '📋';
if (mimeType.includes('text')) return '📃';
return '📁';
}
/**
* Format file size to human readable
*/
private formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Setup image block functionality
*/