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
|
// 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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
@ -950,7 +1395,439 @@ export class DeesWysiwygBlock extends DeesElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup YouTube block functionality
|
||||||
|
*/
|
||||||
|
private setupYouTubeBlock(): void {
|
||||||
|
const youtubeBlock = this.shadowRoot?.querySelector('.block.youtube') as HTMLDivElement;
|
||||||
|
if (!youtubeBlock) return;
|
||||||
|
|
||||||
|
// Handle click to select
|
||||||
|
youtubeBlock.addEventListener('click', (e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (!target.classList.contains('youtube-url-input') && !target.classList.contains('youtube-embed-btn')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
youtubeBlock.focus();
|
||||||
|
this.handlers?.onFocus?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle URL input and embed button
|
||||||
|
const urlInput = youtubeBlock.querySelector('.youtube-url-input') as HTMLInputElement;
|
||||||
|
const embedBtn = youtubeBlock.querySelector('.youtube-embed-btn') as HTMLButtonElement;
|
||||||
|
|
||||||
|
if (urlInput && embedBtn) {
|
||||||
|
const embedVideo = () => {
|
||||||
|
const url = urlInput.value.trim();
|
||||||
|
if (url) {
|
||||||
|
// Extract video ID from YouTube URL
|
||||||
|
const videoId = this.extractYouTubeVideoId(url);
|
||||||
|
if (videoId) {
|
||||||
|
this.block.metadata = { ...this.block.metadata, videoId, url };
|
||||||
|
this.block.content = url; // Store URL as content
|
||||||
|
|
||||||
|
// Re-render the block
|
||||||
|
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = this.renderBlockContent();
|
||||||
|
this.setupYouTubeBlock(); // Re-setup event handlers
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify parent of change
|
||||||
|
this.handlers?.onInput?.(new InputEvent('input'));
|
||||||
|
} else {
|
||||||
|
alert('Invalid YouTube URL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
embedBtn.addEventListener('click', embedVideo);
|
||||||
|
urlInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
embedVideo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle focus/blur
|
||||||
|
youtubeBlock.addEventListener('focus', () => this.handlers?.onFocus?.());
|
||||||
|
youtubeBlock.addEventListener('blur', () => this.handlers?.onBlur?.());
|
||||||
|
|
||||||
|
// Handle keyboard events
|
||||||
|
youtubeBlock.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.handlers?.onKeyDown?.(e);
|
||||||
|
} else {
|
||||||
|
this.handlers?.onKeyDown?.(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup Markdown block functionality
|
||||||
|
*/
|
||||||
|
private setupMarkdownBlock(): void {
|
||||||
|
const markdownBlock = this.shadowRoot?.querySelector('.block.markdown') as HTMLDivElement;
|
||||||
|
if (!markdownBlock) return;
|
||||||
|
|
||||||
|
// Handle click to select
|
||||||
|
markdownBlock.addEventListener('click', (e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (!target.classList.contains('markdown-editor') && !target.classList.contains('toggle-preview')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
markdownBlock.focus();
|
||||||
|
this.handlers?.onFocus?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle preview toggle
|
||||||
|
const toggleBtn = markdownBlock.querySelector('.toggle-preview') as HTMLButtonElement;
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.addEventListener('click', () => {
|
||||||
|
const showPreview = toggleBtn.dataset.active !== 'true';
|
||||||
|
this.block.metadata = { ...this.block.metadata, showPreview };
|
||||||
|
|
||||||
|
// Re-render
|
||||||
|
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = this.renderBlockContent();
|
||||||
|
this.setupMarkdownBlock();
|
||||||
|
|
||||||
|
// If switching to preview, render markdown
|
||||||
|
if (showPreview) {
|
||||||
|
this.renderMarkdownPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle editor input
|
||||||
|
const editor = markdownBlock.querySelector('.markdown-editor') as HTMLTextAreaElement;
|
||||||
|
if (editor) {
|
||||||
|
editor.addEventListener('input', () => {
|
||||||
|
this.block.content = editor.value;
|
||||||
|
this.handlers?.onInput?.(new InputEvent('input'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-resize textarea
|
||||||
|
const autoResize = () => {
|
||||||
|
editor.style.height = 'auto';
|
||||||
|
editor.style.height = editor.scrollHeight + 'px';
|
||||||
|
};
|
||||||
|
editor.addEventListener('input', autoResize);
|
||||||
|
autoResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render preview if needed
|
||||||
|
if (this.block.metadata?.showPreview) {
|
||||||
|
this.renderMarkdownPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle focus/blur
|
||||||
|
markdownBlock.addEventListener('focus', () => this.handlers?.onFocus?.());
|
||||||
|
markdownBlock.addEventListener('blur', () => this.handlers?.onBlur?.());
|
||||||
|
|
||||||
|
// Handle keyboard events
|
||||||
|
markdownBlock.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.handlers?.onKeyDown?.(e);
|
||||||
|
} else {
|
||||||
|
this.handlers?.onKeyDown?.(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup HTML block functionality
|
||||||
|
*/
|
||||||
|
private setupHtmlBlock(): void {
|
||||||
|
const htmlBlock = this.shadowRoot?.querySelector('.block.html') as HTMLDivElement;
|
||||||
|
if (!htmlBlock) return;
|
||||||
|
|
||||||
|
// Handle click to select
|
||||||
|
htmlBlock.addEventListener('click', (e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (!target.classList.contains('html-editor') && !target.classList.contains('toggle-preview')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
htmlBlock.focus();
|
||||||
|
this.handlers?.onFocus?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle preview toggle
|
||||||
|
const toggleBtn = htmlBlock.querySelector('.toggle-preview') as HTMLButtonElement;
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.addEventListener('click', () => {
|
||||||
|
const showPreview = toggleBtn.dataset.active !== 'true';
|
||||||
|
this.block.metadata = { ...this.block.metadata, showPreview };
|
||||||
|
|
||||||
|
// Re-render
|
||||||
|
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = this.renderBlockContent();
|
||||||
|
this.setupHtmlBlock();
|
||||||
|
|
||||||
|
// If switching to preview, render HTML
|
||||||
|
if (showPreview) {
|
||||||
|
this.renderHtmlPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle editor input
|
||||||
|
const editor = htmlBlock.querySelector('.html-editor') as HTMLTextAreaElement;
|
||||||
|
if (editor) {
|
||||||
|
editor.addEventListener('input', () => {
|
||||||
|
this.block.content = editor.value;
|
||||||
|
this.handlers?.onInput?.(new InputEvent('input'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-resize textarea
|
||||||
|
const autoResize = () => {
|
||||||
|
editor.style.height = 'auto';
|
||||||
|
editor.style.height = editor.scrollHeight + 'px';
|
||||||
|
};
|
||||||
|
editor.addEventListener('input', autoResize);
|
||||||
|
autoResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render preview if needed
|
||||||
|
if (this.block.metadata?.showPreview) {
|
||||||
|
this.renderHtmlPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle focus/blur
|
||||||
|
htmlBlock.addEventListener('focus', () => this.handlers?.onFocus?.());
|
||||||
|
htmlBlock.addEventListener('blur', () => this.handlers?.onBlur?.());
|
||||||
|
|
||||||
|
// Handle keyboard events
|
||||||
|
htmlBlock.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.handlers?.onKeyDown?.(e);
|
||||||
|
} else {
|
||||||
|
this.handlers?.onKeyDown?.(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup Attachment block functionality
|
||||||
|
*/
|
||||||
|
private setupAttachmentBlock(): void {
|
||||||
|
const attachmentBlock = this.shadowRoot?.querySelector('.block.attachment') as HTMLDivElement;
|
||||||
|
if (!attachmentBlock) return;
|
||||||
|
|
||||||
|
// Handle click to select
|
||||||
|
attachmentBlock.addEventListener('click', (e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (!target.classList.contains('remove-file')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
attachmentBlock.focus();
|
||||||
|
this.handlers?.onFocus?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle file input
|
||||||
|
const fileInput = attachmentBlock.querySelector('input[type="file"]') as HTMLInputElement;
|
||||||
|
const placeholder = attachmentBlock.querySelector('.attachment-placeholder');
|
||||||
|
const addMoreBtn = attachmentBlock.querySelector('.add-more-files') as HTMLButtonElement;
|
||||||
|
|
||||||
|
const triggerFileInput = () => {
|
||||||
|
if (fileInput) fileInput.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (placeholder) {
|
||||||
|
placeholder.addEventListener('click', triggerFileInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMoreBtn) {
|
||||||
|
addMoreBtn.addEventListener('click', triggerFileInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.addEventListener('change', async (e) => {
|
||||||
|
const files = Array.from((e.target as HTMLInputElement).files || []);
|
||||||
|
if (files.length > 0) {
|
||||||
|
await this.handleFileAttachments(files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle file removal
|
||||||
|
attachmentBlock.addEventListener('click', (e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.classList.contains('remove-file')) {
|
||||||
|
const fileId = target.dataset.fileId;
|
||||||
|
if (fileId) {
|
||||||
|
const files = this.block.metadata?.files || [];
|
||||||
|
this.block.metadata = {
|
||||||
|
...this.block.metadata,
|
||||||
|
files: files.filter((f: any) => f.id !== fileId)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-render
|
||||||
|
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = this.renderBlockContent();
|
||||||
|
this.setupAttachmentBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handlers?.onInput?.(new InputEvent('input'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle drag and drop
|
||||||
|
attachmentBlock.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
attachmentBlock.classList.add('drag-over');
|
||||||
|
});
|
||||||
|
|
||||||
|
attachmentBlock.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
attachmentBlock.classList.remove('drag-over');
|
||||||
|
});
|
||||||
|
|
||||||
|
attachmentBlock.addEventListener('drop', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
attachmentBlock.classList.remove('drag-over');
|
||||||
|
|
||||||
|
const files = Array.from(e.dataTransfer?.files || []);
|
||||||
|
if (files.length > 0) {
|
||||||
|
await this.handleFileAttachments(files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle focus/blur
|
||||||
|
attachmentBlock.addEventListener('focus', () => this.handlers?.onFocus?.());
|
||||||
|
attachmentBlock.addEventListener('blur', () => this.handlers?.onBlur?.());
|
||||||
|
|
||||||
|
// Handle keyboard events
|
||||||
|
attachmentBlock.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.handlers?.onKeyDown?.(e);
|
||||||
|
} else {
|
||||||
|
this.handlers?.onKeyDown?.(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract YouTube video ID from URL
|
||||||
|
*/
|
||||||
|
private extractYouTubeVideoId(url: string): string | null {
|
||||||
|
const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/;
|
||||||
|
const match = url.match(regex);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render Markdown preview
|
||||||
|
*/
|
||||||
|
private async renderMarkdownPreview(): Promise<void> {
|
||||||
|
const preview = this.shadowRoot?.querySelector('.markdown-preview') as HTMLDivElement;
|
||||||
|
if (!preview || !this.block.content) return;
|
||||||
|
|
||||||
|
// Simple markdown to HTML conversion (you might want to use a proper markdown parser)
|
||||||
|
let html = this.block.content
|
||||||
|
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||||
|
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||||
|
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||||
|
.replace(/\*\*(.*)\*\*/g, '<strong>$1</strong>')
|
||||||
|
.replace(/\*(.*)\*/g, '<em>$1</em>')
|
||||||
|
.replace(/\[([^\]]*)\]\(([^\)]*)\)/g, '<a href="$2">$1</a>')
|
||||||
|
.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
preview.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render HTML preview
|
||||||
|
*/
|
||||||
|
private renderHtmlPreview(): void {
|
||||||
|
const preview = this.shadowRoot?.querySelector('.html-preview') as HTMLDivElement;
|
||||||
|
if (!preview || !this.block.content) return;
|
||||||
|
|
||||||
|
// Render HTML in a sandboxed way
|
||||||
|
preview.innerHTML = this.block.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle file attachments
|
||||||
|
*/
|
||||||
|
private async handleFileAttachments(files: File[]): Promise<void> {
|
||||||
|
const existingFiles = this.block.metadata?.files || [];
|
||||||
|
const newFiles: any[] = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
// Convert to base64 for storage (in production, upload to server)
|
||||||
|
const reader = new FileReader();
|
||||||
|
const base64 = await new Promise<string>((resolve) => {
|
||||||
|
reader.onload = (e) => resolve(e.target?.result as string);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
newFiles.push({
|
||||||
|
id: `file-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
data: base64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.block.metadata = {
|
||||||
|
...this.block.metadata,
|
||||||
|
files: [...existingFiles, ...newFiles]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-render
|
||||||
|
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = this.renderBlockContent();
|
||||||
|
this.setupAttachmentBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handlers?.onInput?.(new InputEvent('input'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file icon based on mime type
|
||||||
|
*/
|
||||||
|
private getFileIcon(mimeType: string): string {
|
||||||
|
if (mimeType.startsWith('image/')) return '🖼️';
|
||||||
|
if (mimeType.startsWith('video/')) return '🎥';
|
||||||
|
if (mimeType.startsWith('audio/')) return '🎵';
|
||||||
|
if (mimeType.includes('pdf')) return '📄';
|
||||||
|
if (mimeType.includes('zip') || mimeType.includes('compressed')) return '🗄️';
|
||||||
|
if (mimeType.includes('sheet') || mimeType.includes('excel')) return '📊';
|
||||||
|
if (mimeType.includes('document') || mimeType.includes('word')) return '📝';
|
||||||
|
if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) return '📋';
|
||||||
|
if (mimeType.includes('text')) return '📃';
|
||||||
|
return '📁';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format file size to human readable
|
||||||
|
*/
|
||||||
|
private formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup image block functionality
|
* Setup image block functionality
|
||||||
*/
|
*/
|
||||||
|
@ -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 ? `` : '';
|
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:
|
default:
|
||||||
return block.content;
|
return block.content;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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: '📎' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user