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

@ -131,7 +131,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
// Listen for custom selection events from blocks // Listen for custom selection events from blocks
this.addEventListener('block-text-selected', (e: CustomEvent) => { this.addEventListener('block-text-selected', (e: CustomEvent) => {
console.log('Received block-text-selected event:', e.detail);
if (!this.slashMenu.visible && e.detail.hasSelection && e.detail.text.length > 0) { if (!this.slashMenu.visible && e.detail.hasSelection && e.detail.text.length > 0) {
this.selectedText = e.detail.text; this.selectedText = e.detail.text;
@ -143,7 +142,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
y: Math.max(45, e.detail.rect.top - 45) y: Math.max(45, e.detail.rect.top - 45)
}; };
console.log('Showing formatting menu at:', coords);
// Show the formatting menu at the calculated position // Show the formatting menu at the calculated position
this.formattingMenu.show( this.formattingMenu.show(
@ -533,6 +531,20 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
// For image blocks, clear content and set empty metadata // For image blocks, clear content and set empty metadata
currentBlock.content = ''; currentBlock.content = '';
currentBlock.metadata = { url: '', loading: false }; currentBlock.metadata = { url: '', loading: false };
} else if (type === 'youtube') {
// For YouTube blocks, clear content and set empty metadata
currentBlock.content = '';
currentBlock.metadata = { videoId: '', url: '' };
} else if (type === 'markdown') {
// For Markdown blocks, preserve content and default to edit mode
currentBlock.metadata = { showPreview: false };
} else if (type === 'html') {
// For HTML blocks, preserve content and default to edit mode
currentBlock.metadata = { showPreview: false };
} else if (type === 'attachment') {
// For attachment blocks, clear content and set empty files array
currentBlock.content = '';
currentBlock.metadata = { files: [] };
} else { } else {
// For all other block types, ensure content is clean // For all other block types, ensure content is clean
currentBlock.content = currentBlock.content || ''; currentBlock.content = currentBlock.content || '';
@ -556,10 +568,10 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
blockComponent.focusListItem(); blockComponent.focusListItem();
} }
}); });
} else if (type !== 'divider' && type !== 'image') { } else if (type !== 'divider' && type !== 'image' && type !== 'youtube' && type !== 'markdown' && type !== 'html' && type !== 'attachment') {
this.blockOperations.focusBlock(currentBlock.id, 'start'); this.blockOperations.focusBlock(currentBlock.id, 'start');
} else if (type === 'image') { } else if (type === 'image' || type === 'youtube' || type === 'markdown' || type === 'html' || type === 'attachment') {
// Focus the image block (which will show the upload interface) // Focus the non-editable block
this.blockOperations.focusBlock(currentBlock.id); this.blockOperations.focusBlock(currentBlock.id);
} }
} }
@ -733,7 +745,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
private updateFormattingMenuPosition(): void { private updateFormattingMenuPosition(): void {
console.log('updateFormattingMenuPosition called');
// Get all shadow roots // Get all shadow roots
const shadowRoots: ShadowRoot[] = []; const shadowRoots: ShadowRoot[] = [];
@ -749,7 +760,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
}); });
const coords = WysiwygFormatting.getSelectionCoordinates(...shadowRoots); const coords = WysiwygFormatting.getSelectionCoordinates(...shadowRoots);
console.log('Selection coordinates:', coords);
if (coords) { if (coords) {
// Show the global formatting menu at absolute coordinates // Show the global formatting menu at absolute coordinates
@ -758,7 +768,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
async (command: string) => await this.applyFormat(command) async (command: string) => await this.applyFormat(command)
); );
} else { } else {
console.log('No coordinates found');
} }
} }
@ -924,7 +933,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
* Undo the last action * Undo the last action
*/ */
private undo(): void { private undo(): void {
console.log('Undo triggered');
const state = this.history.undo(); const state = this.history.undo();
if (state) { if (state) {
this.restoreState(state); this.restoreState(state);
@ -935,7 +943,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
* Redo the next action * Redo the next action
*/ */
private redo(): void { private redo(): void {
console.log('Redo triggered');
const state = this.history.redo(); const state = this.history.redo();
if (state) { if (state) {
this.restoreState(state); this.restoreState(state);

View File

@ -368,12 +368,345 @@ export class DeesWysiwygBlock extends DeesElement {
input[type="file"] { input[type="file"] {
display: none; 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 { protected shouldUpdate(changedProperties: Map<string, any>): boolean {
// If selection state changed, we need to update for non-editable blocks // 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 // For non-editable blocks, we need to update the selected class
const element = this.shadowRoot?.querySelector('.block') as HTMLElement; const element = this.shadowRoot?.querySelector('.block') as HTMLElement;
if (element) { if (element) {
@ -416,6 +749,18 @@ export class DeesWysiwygBlock extends DeesElement {
} else if (this.block.type === 'divider') { } else if (this.block.type === 'divider') {
this.setupDividerBlock(); this.setupDividerBlock();
return; // Divider blocks don't need the standard editable setup 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 // Now find the actual editable block element
@ -470,7 +815,6 @@ export class DeesWysiwygBlock extends DeesElement {
const pos = this.getCursorPosition(editableBlock); const pos = this.getCursorPosition(editableBlock);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('Cursor position after mouseup:', pos);
} }
// Selection will be handled by selectionchange event // Selection will be handled by selectionchange event
@ -483,7 +827,6 @@ export class DeesWysiwygBlock extends DeesElement {
const pos = this.getCursorPosition(editableBlock); const pos = this.getCursorPosition(editableBlock);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('Cursor position after click:', pos);
} }
}, 0); }, 0);
}); });
@ -538,7 +881,6 @@ export class DeesWysiwygBlock extends DeesElement {
if (startInBlock || endInBlock) { if (startInBlock || endInBlock) {
if (selectedText !== this.lastSelectedText) { if (selectedText !== this.lastSelectedText) {
this.lastSelectedText = selectedText; this.lastSelectedText = selectedText;
console.log('✅ Selection detected in block using getComposedRanges:', selectedText);
// Create range and get rect // Create range and get rect
const range = WysiwygSelection.createRangeFromInfo(selectionInfo); 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 placeholder = this.getPlaceholder();
const selectedClass = this.isSelected ? ' selected' : ''; const selectedClass = this.isSelected ? ' selected' : '';
return ` return `
@ -697,16 +1146,11 @@ export class DeesWysiwygBlock extends DeesElement {
public focus(): void { public focus(): void {
// Handle non-editable blocks // Handle non-editable blocks
if (this.block?.type === 'image') { const nonEditableTypes = ['image', 'divider', 'youtube', 'markdown', 'html', 'attachment'];
const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement; if (this.block && nonEditableTypes.includes(this.block.type)) {
if (imageBlock) { const blockElement = this.shadowRoot?.querySelector(`.block.${this.block.type}`) as HTMLDivElement;
imageBlock.focus(); if (blockElement) {
} blockElement.focus();
return;
} else if (this.block?.type === 'divider') {
const dividerBlock = this.shadowRoot?.querySelector('.block.divider') as HTMLDivElement;
if (dividerBlock) {
dividerBlock.focus();
} }
return; return;
} }
@ -735,7 +1179,8 @@ export class DeesWysiwygBlock extends DeesElement {
public focusWithCursor(position: 'start' | 'end' | number = 'end'): void { public focusWithCursor(position: 'start' | 'end' | number = 'end'): void {
// Non-editable blocks don't support cursor positioning // 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(); this.focus();
return; return;
} }
@ -951,6 +1396,438 @@ 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 * Setup image block functionality
*/ */

View File

@ -7,6 +7,14 @@ export class WysiwygConverters {
return div.innerHTML; return div.innerHTML;
} }
static 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];
}
static getHtmlOutput(blocks: IBlock[]): string { static getHtmlOutput(blocks: IBlock[]): string {
return blocks.map(block => { return blocks.map(block => {
// Check if content already contains HTML formatting // Check if content already contains HTML formatting
@ -44,6 +52,29 @@ export class WysiwygConverters {
return `<img src="${imageUrl}" alt="${altText}" />`; return `<img src="${imageUrl}" alt="${altText}" />`;
} }
return ''; return '';
case 'youtube':
const videoId = block.metadata?.videoId;
if (videoId) {
return `<iframe width="560" height="315" src="https://www.youtube.com/embed/${videoId}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
}
return '';
case 'markdown':
// Return the raw markdown content wrapped in a div
return `<div class="markdown-content">${this.escapeHtml(block.content)}</div>`;
case 'html':
// Return the raw HTML content (already HTML)
return block.content;
case 'attachment':
const files = block.metadata?.files || [];
if (files.length > 0) {
return `<div class="attachments">${files.map((file: any) =>
`<div class="attachment-item" data-file-id="${file.id}">
<a href="${file.data}" download="${file.name}">${this.escapeHtml(file.name)}</a>
<span class="file-size">(${this.formatFileSize(file.size)})</span>
</div>`
).join('')}</div>`;
}
return '';
default: default:
return `<p>${content}</p>`; return `<p>${content}</p>`;
} }
@ -78,6 +109,22 @@ export class WysiwygConverters {
const imageUrl = block.metadata?.url; const imageUrl = block.metadata?.url;
const altText = block.content || 'Image'; const altText = block.content || 'Image';
return imageUrl ? `![${altText}](${imageUrl})` : ''; return imageUrl ? `![${altText}](${imageUrl})` : '';
case 'youtube':
const videoId = block.metadata?.videoId;
const url = block.metadata?.url || (videoId ? `https://youtube.com/watch?v=${videoId}` : '');
return url ? `[YouTube Video](${url})` : '';
case 'markdown':
// Return the raw markdown content
return block.content;
case 'html':
// Return as HTML comment in markdown
return `<!-- HTML Block\n${block.content}\n-->`;
case 'attachment':
const files = block.metadata?.files || [];
if (files.length > 0) {
return files.map((file: any) => `- [${file.name}](${file.data})`).join('\n');
}
return '';
default: default:
return block.content; return block.content;
} }

View File

@ -233,8 +233,9 @@ export class WysiwygKeyboardHandler {
private async handleBackspace(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleBackspace(e: KeyboardEvent, block: IBlock): Promise<void> {
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
// Handle non-editable blocks (divider, image) // Handle non-editable blocks
if (block.type === 'divider' || block.type === 'image') { const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
// If it's the only block, delete it and create a new paragraph // If it's the only block, delete it and create a new paragraph
@ -311,13 +312,13 @@ export class WysiwygKeyboardHandler {
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
if (prevBlock) { if (prevBlock) {
// If previous block is non-editable (divider/image), select it first // If previous block is non-editable, select it first
if (prevBlock.type === 'divider' || prevBlock.type === 'image') { const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
if (nonEditableTypes.includes(prevBlock.type)) {
await blockOps.focusBlock(prevBlock.id); await blockOps.focusBlock(prevBlock.id);
return; return;
} }
console.log('Backspace at start: Merging with previous block');
// Save checkpoint for undo // Save checkpoint for undo
this.component.saveToHistory(false); this.component.saveToHistory(false);
@ -397,8 +398,9 @@ export class WysiwygKeyboardHandler {
private async handleDelete(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleDelete(e: KeyboardEvent, block: IBlock): Promise<void> {
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
// Handle non-editable blocks (divider, image) - same as backspace // Handle non-editable blocks - same as backspace
if (block.type === 'divider' || block.type === 'image') { const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
// If it's the only block, delete it and create a new paragraph // If it's the only block, delete it and create a new paragraph
@ -435,9 +437,10 @@ export class WysiwygKeyboardHandler {
blockOps.removeBlock(block.id); blockOps.removeBlock(block.id);
// Focus the appropriate block // Focus the appropriate block
if (nextBlock && nextBlock.type !== 'divider' && nextBlock.type !== 'image') { const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
if (nextBlock && !nonEditableTypes.includes(nextBlock.type)) {
await blockOps.focusBlock(nextBlock.id, 'start'); await blockOps.focusBlock(nextBlock.id, 'start');
} else if (prevBlock && prevBlock.type !== 'divider' && prevBlock.type !== 'image') { } else if (prevBlock && !nonEditableTypes.includes(prevBlock.type)) {
await blockOps.focusBlock(prevBlock.id, 'end'); await blockOps.focusBlock(prevBlock.id, 'end');
} else if (nextBlock) { } else if (nextBlock) {
// If next block is also non-editable, just select it // If next block is also non-editable, just select it
@ -474,7 +477,8 @@ export class WysiwygKeyboardHandler {
if (cursorPos === textLength) { if (cursorPos === textLength) {
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
if (nextBlock && (nextBlock.type === 'divider' || nextBlock.type === 'image')) { const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
if (nextBlock && nonEditableTypes.includes(nextBlock.type)) {
e.preventDefault(); e.preventDefault();
await blockOps.focusBlock(nextBlock.id); await blockOps.focusBlock(nextBlock.id);
return; return;
@ -489,13 +493,14 @@ export class WysiwygKeyboardHandler {
*/ */
private async handleArrowUp(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowUp(e: KeyboardEvent, block: IBlock): Promise<void> {
// For non-editable blocks, always navigate to previous block // For non-editable blocks, always navigate to previous block
if (block.type === 'divider' || block.type === 'image') { const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
if (prevBlock) { if (prevBlock) {
await blockOps.focusBlock(prevBlock.id, prevBlock.type === 'divider' || prevBlock.type === 'image' ? undefined : 'end'); await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end');
} }
return; return;
} }
@ -522,14 +527,13 @@ export class WysiwygKeyboardHandler {
// Check if we're on the first line // Check if we're on the first line
if (this.isOnFirstLine(selectionInfo, target, ...shadowRoots)) { if (this.isOnFirstLine(selectionInfo, target, ...shadowRoots)) {
console.log('ArrowUp: On first line, navigating to previous block');
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
if (prevBlock) { if (prevBlock) {
console.log('ArrowUp: Focusing previous block:', prevBlock.id); const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
await blockOps.focusBlock(prevBlock.id, prevBlock.type === 'divider' || prevBlock.type === 'image' ? undefined : 'end'); await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end');
} }
} }
// Otherwise, let browser handle normal navigation // Otherwise, let browser handle normal navigation
@ -540,13 +544,15 @@ export class WysiwygKeyboardHandler {
*/ */
private async handleArrowDown(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowDown(e: KeyboardEvent, block: IBlock): Promise<void> {
// For non-editable blocks, always navigate to next block // For non-editable blocks, always navigate to next block
if (block.type === 'divider' || block.type === 'image') { const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
if (nextBlock) { if (nextBlock) {
await blockOps.focusBlock(nextBlock.id, nextBlock.type === 'divider' || nextBlock.type === 'image' ? undefined : 'start'); const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start');
} }
return; return;
} }
@ -573,14 +579,13 @@ export class WysiwygKeyboardHandler {
// Check if we're on the last line // Check if we're on the last line
if (this.isOnLastLine(selectionInfo, target, ...shadowRoots)) { if (this.isOnLastLine(selectionInfo, target, ...shadowRoots)) {
console.log('ArrowDown: On last line, navigating to next block');
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
if (nextBlock) { if (nextBlock) {
console.log('ArrowDown: Focusing next block:', nextBlock.id); const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
await blockOps.focusBlock(nextBlock.id, nextBlock.type === 'divider' || nextBlock.type === 'image' ? undefined : 'start'); await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start');
} }
} }
// Otherwise, let browser handle normal navigation // Otherwise, let browser handle normal navigation
@ -607,13 +612,15 @@ export class WysiwygKeyboardHandler {
*/ */
private async handleArrowLeft(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowLeft(e: KeyboardEvent, block: IBlock): Promise<void> {
// For non-editable blocks, navigate to previous block // For non-editable blocks, navigate to previous block
if (block.type === 'divider' || block.type === 'image') { const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
if (prevBlock) { if (prevBlock) {
await blockOps.focusBlock(prevBlock.id, prevBlock.type === 'divider' || prevBlock.type === 'image' ? undefined : 'end'); const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end');
} }
return; return;
} }
@ -640,16 +647,15 @@ export class WysiwygKeyboardHandler {
// Check if cursor is at the beginning of the block // Check if cursor is at the beginning of the block
const cursorPos = WysiwygSelection.getCursorPositionInElement(target, ...shadowRoots); const cursorPos = WysiwygSelection.getCursorPositionInElement(target, ...shadowRoots);
console.log('ArrowLeft: Cursor position:', cursorPos, 'in block:', block.id);
if (cursorPos === 0) { if (cursorPos === 0) {
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
console.log('ArrowLeft: At start, previous block:', prevBlock?.id);
if (prevBlock) { if (prevBlock) {
e.preventDefault(); e.preventDefault();
await blockOps.focusBlock(prevBlock.id, prevBlock.type === 'divider' || prevBlock.type === 'image' ? undefined : 'end'); const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end');
} }
} }
// Otherwise, let the browser handle normal left arrow navigation // Otherwise, let the browser handle normal left arrow navigation
@ -660,13 +666,15 @@ export class WysiwygKeyboardHandler {
*/ */
private async handleArrowRight(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowRight(e: KeyboardEvent, block: IBlock): Promise<void> {
// For non-editable blocks, navigate to next block // For non-editable blocks, navigate to next block
if (block.type === 'divider' || block.type === 'image') { const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
if (nextBlock) { if (nextBlock) {
await blockOps.focusBlock(nextBlock.id, nextBlock.type === 'divider' || nextBlock.type === 'image' ? undefined : 'start'); const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start');
} }
return; return;
} }
@ -701,7 +709,8 @@ export class WysiwygKeyboardHandler {
if (nextBlock) { if (nextBlock) {
e.preventDefault(); e.preventDefault();
await blockOps.focusBlock(nextBlock.id, nextBlock.type === 'divider' || nextBlock.type === 'image' ? undefined : 'start'); const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start');
} }
} }
// Otherwise, let the browser handle normal right arrow navigation // Otherwise, let the browser handle normal right arrow navigation

View File

@ -58,6 +58,10 @@ export class WysiwygShortcuts {
{ type: 'list', label: 'List', icon: '•' }, { type: 'list', label: 'List', icon: '•' },
{ type: 'image', label: 'Image', icon: '🖼' }, { type: 'image', label: 'Image', icon: '🖼' },
{ type: 'divider', label: 'Divider', icon: '—' }, { type: 'divider', label: 'Divider', icon: '—' },
{ type: 'youtube', label: 'YouTube', icon: '▶️' },
{ type: 'markdown', label: 'Markdown', icon: 'M↓' },
{ type: 'html', label: 'HTML', icon: '</>' },
{ type: 'attachment', label: 'File Attachment', icon: '📎' },
]; ];
} }

View File

@ -1,6 +1,6 @@
export interface IBlock { export interface IBlock {
id: string; id: string;
type: 'paragraph' | 'heading-1' | 'heading-2' | 'heading-3' | 'image' | 'code' | 'quote' | 'list' | 'divider'; type: 'paragraph' | 'heading-1' | 'heading-2' | 'heading-3' | 'image' | 'code' | 'quote' | 'list' | 'divider' | 'youtube' | 'markdown' | 'html' | 'attachment';
content: string; content: string;
metadata?: any; metadata?: any;
} }