feat(wysiwyg): Add more block types
This commit is contained in:
@ -131,7 +131,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
|
||||
// Listen for custom selection events from blocks
|
||||
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) {
|
||||
this.selectedText = e.detail.text;
|
||||
@ -143,7 +142,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
y: Math.max(45, e.detail.rect.top - 45)
|
||||
};
|
||||
|
||||
console.log('Showing formatting menu at:', coords);
|
||||
|
||||
// Show the formatting menu at the calculated position
|
||||
this.formattingMenu.show(
|
||||
@ -533,6 +531,20 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
// For image blocks, clear content and set empty metadata
|
||||
currentBlock.content = '';
|
||||
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 {
|
||||
// For all other block types, ensure content is clean
|
||||
currentBlock.content = currentBlock.content || '';
|
||||
@ -556,10 +568,10 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
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');
|
||||
} else if (type === 'image') {
|
||||
// Focus the image block (which will show the upload interface)
|
||||
} else if (type === 'image' || type === 'youtube' || type === 'markdown' || type === 'html' || type === 'attachment') {
|
||||
// Focus the non-editable block
|
||||
this.blockOperations.focusBlock(currentBlock.id);
|
||||
}
|
||||
}
|
||||
@ -733,7 +745,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
|
||||
|
||||
private updateFormattingMenuPosition(): void {
|
||||
console.log('updateFormattingMenuPosition called');
|
||||
|
||||
// Get all shadow roots
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
@ -749,7 +760,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
});
|
||||
|
||||
const coords = WysiwygFormatting.getSelectionCoordinates(...shadowRoots);
|
||||
console.log('Selection coordinates:', coords);
|
||||
|
||||
if (coords) {
|
||||
// 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)
|
||||
);
|
||||
} else {
|
||||
console.log('No coordinates found');
|
||||
}
|
||||
}
|
||||
|
||||
@ -924,7 +933,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
* Undo the last action
|
||||
*/
|
||||
private undo(): void {
|
||||
console.log('Undo triggered');
|
||||
const state = this.history.undo();
|
||||
if (state) {
|
||||
this.restoreState(state);
|
||||
@ -935,7 +943,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
* Redo the next action
|
||||
*/
|
||||
private redo(): void {
|
||||
console.log('Redo triggered');
|
||||
const state = this.history.redo();
|
||||
if (state) {
|
||||
this.restoreState(state);
|
||||
|
@ -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;
|
||||
}
|
||||
@ -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
|
||||
*/
|
||||
|
@ -7,6 +7,14 @@ export class WysiwygConverters {
|
||||
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 {
|
||||
return blocks.map(block => {
|
||||
// Check if content already contains HTML formatting
|
||||
@ -44,6 +52,29 @@ export class WysiwygConverters {
|
||||
return `<img src="${imageUrl}" alt="${altText}" />`;
|
||||
}
|
||||
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:
|
||||
return `<p>${content}</p>`;
|
||||
}
|
||||
@ -78,6 +109,22 @@ export class WysiwygConverters {
|
||||
const imageUrl = block.metadata?.url;
|
||||
const altText = block.content || 'Image';
|
||||
return 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:
|
||||
return block.content;
|
||||
}
|
||||
|
@ -233,8 +233,9 @@ export class WysiwygKeyboardHandler {
|
||||
private async handleBackspace(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
const blockOps = this.component.blockOperations;
|
||||
|
||||
// Handle non-editable blocks (divider, image)
|
||||
if (block.type === 'divider' || block.type === 'image') {
|
||||
// Handle non-editable blocks
|
||||
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
|
||||
if (nonEditableTypes.includes(block.type)) {
|
||||
e.preventDefault();
|
||||
|
||||
// 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);
|
||||
|
||||
if (prevBlock) {
|
||||
// If previous block is non-editable (divider/image), select it first
|
||||
if (prevBlock.type === 'divider' || prevBlock.type === 'image') {
|
||||
// If previous block is non-editable, select it first
|
||||
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
|
||||
if (nonEditableTypes.includes(prevBlock.type)) {
|
||||
await blockOps.focusBlock(prevBlock.id);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Backspace at start: Merging with previous block');
|
||||
|
||||
// Save checkpoint for undo
|
||||
this.component.saveToHistory(false);
|
||||
@ -397,8 +398,9 @@ export class WysiwygKeyboardHandler {
|
||||
private async handleDelete(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
const blockOps = this.component.blockOperations;
|
||||
|
||||
// Handle non-editable blocks (divider, image) - same as backspace
|
||||
if (block.type === 'divider' || block.type === 'image') {
|
||||
// Handle non-editable blocks - same as backspace
|
||||
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
|
||||
if (nonEditableTypes.includes(block.type)) {
|
||||
e.preventDefault();
|
||||
|
||||
// If it's the only block, delete it and create a new paragraph
|
||||
@ -435,9 +437,10 @@ export class WysiwygKeyboardHandler {
|
||||
blockOps.removeBlock(block.id);
|
||||
|
||||
// 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');
|
||||
} else if (prevBlock && prevBlock.type !== 'divider' && prevBlock.type !== 'image') {
|
||||
} else if (prevBlock && !nonEditableTypes.includes(prevBlock.type)) {
|
||||
await blockOps.focusBlock(prevBlock.id, 'end');
|
||||
} else if (nextBlock) {
|
||||
// If next block is also non-editable, just select it
|
||||
@ -474,7 +477,8 @@ export class WysiwygKeyboardHandler {
|
||||
if (cursorPos === textLength) {
|
||||
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();
|
||||
await blockOps.focusBlock(nextBlock.id);
|
||||
return;
|
||||
@ -489,13 +493,14 @@ export class WysiwygKeyboardHandler {
|
||||
*/
|
||||
private async handleArrowUp(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
// 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();
|
||||
const blockOps = this.component.blockOperations;
|
||||
const prevBlock = blockOps.getPreviousBlock(block.id);
|
||||
|
||||
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;
|
||||
}
|
||||
@ -522,14 +527,13 @@ export class WysiwygKeyboardHandler {
|
||||
|
||||
// Check if we're on the first line
|
||||
if (this.isOnFirstLine(selectionInfo, target, ...shadowRoots)) {
|
||||
console.log('ArrowUp: On first line, navigating to previous block');
|
||||
e.preventDefault();
|
||||
const blockOps = this.component.blockOperations;
|
||||
const prevBlock = blockOps.getPreviousBlock(block.id);
|
||||
|
||||
if (prevBlock) {
|
||||
console.log('ArrowUp: Focusing previous block:', prevBlock.id);
|
||||
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 browser handle normal navigation
|
||||
@ -540,13 +544,15 @@ export class WysiwygKeyboardHandler {
|
||||
*/
|
||||
private async handleArrowDown(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
// 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();
|
||||
const blockOps = this.component.blockOperations;
|
||||
const nextBlock = blockOps.getNextBlock(block.id);
|
||||
|
||||
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;
|
||||
}
|
||||
@ -573,14 +579,13 @@ export class WysiwygKeyboardHandler {
|
||||
|
||||
// Check if we're on the last line
|
||||
if (this.isOnLastLine(selectionInfo, target, ...shadowRoots)) {
|
||||
console.log('ArrowDown: On last line, navigating to next block');
|
||||
e.preventDefault();
|
||||
const blockOps = this.component.blockOperations;
|
||||
const nextBlock = blockOps.getNextBlock(block.id);
|
||||
|
||||
if (nextBlock) {
|
||||
console.log('ArrowDown: Focusing next block:', nextBlock.id);
|
||||
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 browser handle normal navigation
|
||||
@ -607,13 +612,15 @@ export class WysiwygKeyboardHandler {
|
||||
*/
|
||||
private async handleArrowLeft(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
// 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();
|
||||
const blockOps = this.component.blockOperations;
|
||||
const prevBlock = blockOps.getPreviousBlock(block.id);
|
||||
|
||||
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;
|
||||
}
|
||||
@ -640,16 +647,15 @@ export class WysiwygKeyboardHandler {
|
||||
|
||||
// Check if cursor is at the beginning of the block
|
||||
const cursorPos = WysiwygSelection.getCursorPositionInElement(target, ...shadowRoots);
|
||||
console.log('ArrowLeft: Cursor position:', cursorPos, 'in block:', block.id);
|
||||
|
||||
if (cursorPos === 0) {
|
||||
const blockOps = this.component.blockOperations;
|
||||
const prevBlock = blockOps.getPreviousBlock(block.id);
|
||||
console.log('ArrowLeft: At start, previous block:', prevBlock?.id);
|
||||
|
||||
if (prevBlock) {
|
||||
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
|
||||
@ -660,13 +666,15 @@ export class WysiwygKeyboardHandler {
|
||||
*/
|
||||
private async handleArrowRight(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
// 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();
|
||||
const blockOps = this.component.blockOperations;
|
||||
const nextBlock = blockOps.getNextBlock(block.id);
|
||||
|
||||
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;
|
||||
}
|
||||
@ -701,7 +709,8 @@ export class WysiwygKeyboardHandler {
|
||||
|
||||
if (nextBlock) {
|
||||
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
|
||||
|
@ -58,6 +58,10 @@ export class WysiwygShortcuts {
|
||||
{ type: 'list', label: 'List', icon: '•' },
|
||||
{ type: 'image', label: 'Image', 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: '📎' },
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
export interface IBlock {
|
||||
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;
|
||||
metadata?: any;
|
||||
}
|
||||
|
Reference in New Issue
Block a user