Improve Wysiwyg editor

This commit is contained in:
Juergen Kunz
2025-06-24 07:21:09 +00:00
parent 7ce282c500
commit e4a042907a
2 changed files with 68 additions and 165 deletions

View File

@ -108,51 +108,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
// Add global selection listener
console.log('Adding selectionchange listener');
document.addEventListener('selectionchange', this.selectionChangeHandler);
// Set initial content for blocks after a brief delay to ensure DOM is ready
await this.updateComplete;
setTimeout(() => {
this.setBlockContents();
}, 50);
}
updated(changedProperties: Map<string, any>) {
// When blocks change (e.g., from setValue), update DOM content
if (changedProperties.has('blocks')) {
// Wait for render to complete
setTimeout(() => {
this.setBlockContents();
}, 50);
}
}
private setBlockContents() {
// Set content for blocks that aren't being edited and don't already have content
this.blocks.forEach(block => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${block.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement && document.activeElement !== blockElement && block.type !== 'divider') {
// Only set content if the element is empty or has placeholder text
const currentContent = blockElement.textContent || '';
const isEmpty = !currentContent || currentContent === 'Type \'/\' for commands...' ||
currentContent === 'Heading 1' || currentContent === 'Heading 2' ||
currentContent === 'Heading 3' || currentContent === 'Quote' ||
currentContent === 'Code block';
if (isEmpty && block.content) {
if (block.type === 'list') {
blockElement.innerHTML = WysiwygBlocks.renderListContent(block.content, block.metadata);
} else if (block.content.includes('<') && block.content.includes('>')) {
// Content contains HTML formatting
blockElement.innerHTML = block.content;
} else {
blockElement.textContent = block.content;
}
}
}
}
});
}
render(): TemplateResult {
@ -276,12 +231,17 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
if (listElement) {
block.metadata = { listType: listElement.tagName.toLowerCase() === 'ol' ? 'ordered' : 'bullet' };
}
} else {
} else if (block.type === 'code') {
// For code blocks, preserve the exact text content
block.content = target.textContent || '';
} else {
// For other blocks, preserve HTML formatting
block.content = target.innerHTML || '';
}
// Check for block type change intents
const detectedType = this.detectBlockTypeIntent(block.content);
// Check for block type change intents (use text content for detection, not HTML)
const textContent = target.textContent || '';
const detectedType = this.detectBlockTypeIntent(textContent);
// Only process if the detected type is different from current type
if (detectedType && detectedType.type !== block.type) {
@ -311,23 +271,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
block.content = ' ';
// Create a new paragraph block after the divider
const blockIndex = this.blocks.findIndex(b => b.id === block.id);
const newBlock: IBlock = {
id: WysiwygShortcuts.generateBlockId(),
type: 'paragraph',
content: '',
};
this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)];
setTimeout(() => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement) {
blockElement.focus();
}
}
});
const newBlock = this.createNewBlock();
this.insertBlockAfter(block, newBlock);
this.updateValue();
this.requestUpdate();
@ -366,9 +311,9 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
}
// Check for slash commands at the beginning of any block
if (block.content === '/' || (block.content.startsWith('/') && this.showSlashMenu)) {
if (textContent === '/' || (textContent.startsWith('/') && this.showSlashMenu)) {
// Only show menu on initial '/', or update filter if already showing
if (!this.showSlashMenu && block.content === '/') {
if (!this.showSlashMenu && textContent === '/') {
this.showSlashMenu = true;
this.slashMenuSelectedIndex = 0;
const rect = target.getBoundingClientRect();
@ -378,8 +323,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
y: rect.bottom - containerRect.top + 4
};
}
this.slashMenuFilter = block.content.slice(1);
} else if (!block.content.startsWith('/')) {
this.slashMenuFilter = textContent.slice(1);
} else if (!textContent.startsWith('/')) {
this.closeSlashMenu();
}
@ -441,28 +386,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
if (e.shiftKey) {
// Shift+Enter in code blocks creates a new block
e.preventDefault();
const blockIndex = this.blocks.findIndex(b => b.id === block.id);
const newBlock: IBlock = {
id: WysiwygShortcuts.generateBlockId(),
type: 'paragraph',
content: '',
};
this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)];
this.updateValue();
setTimeout(() => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement) {
blockElement.focus();
WysiwygBlocks.setCursorToStart(blockElement);
}
}
});
const newBlock = this.createNewBlock();
this.insertBlockAfter(block, newBlock);
}
// For normal Enter in code blocks, let the browser handle it (creates new line)
return;
@ -481,25 +406,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
if (currentLi && currentLi.textContent === '') {
// Empty list item - exit list mode
e.preventDefault();
const blockIndex = this.blocks.findIndex(b => b.id === block.id);
const newBlock: IBlock = {
id: WysiwygShortcuts.generateBlockId(),
type: 'paragraph',
content: '',
};
this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)];
setTimeout(() => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement) {
blockElement.focus();
WysiwygBlocks.setCursorToStart(blockElement);
}
}
});
const newBlock = this.createNewBlock();
this.insertBlockAfter(block, newBlock);
}
// Otherwise, let the browser handle creating new list items
}
@ -507,28 +415,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
}
e.preventDefault();
const blockIndex = this.blocks.findIndex(b => b.id === block.id);
const newBlock: IBlock = {
id: WysiwygShortcuts.generateBlockId(),
type: 'paragraph',
content: '',
};
this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)];
this.updateValue();
setTimeout(() => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement) {
blockElement.focus();
WysiwygBlocks.setCursorToStart(blockElement);
}
}
});
const newBlock = this.createNewBlock();
this.insertBlockAfter(block, newBlock);
}
} else if (e.key === 'Backspace' && block.content === '' && this.blocks.length > 1) {
e.preventDefault();
@ -584,9 +472,11 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
if (this.showSlashMenu && this.selectedBlockId) {
// Clear the slash command from the content if menu is closing without selection
const currentBlock = this.blocks.find(b => b.id === this.selectedBlockId);
if (currentBlock && currentBlock.content.startsWith('/')) {
const blockElement = this.shadowRoot!.querySelector(`[data-block-id="${currentBlock.id}"]`) as HTMLDivElement;
if (blockElement) {
if (currentBlock) {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${currentBlock.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement && (blockElement.textContent || '').startsWith('/')) {
// Clear the slash command text
blockElement.textContent = '';
currentBlock.content = '';
@ -595,6 +485,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
}
}
}
}
this.showSlashMenu = false;
this.slashMenuFilter = '';
@ -672,11 +563,40 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
}
}
private createNewBlock(type: IBlock['type'] = 'paragraph', content: string = '', metadata?: any): IBlock {
return {
id: WysiwygShortcuts.generateBlockId(),
type,
content,
...(metadata && { metadata })
};
}
private insertBlockAfter(afterBlock: IBlock, newBlock: IBlock, focusNewBlock: boolean = true): void {
const blockIndex = this.blocks.findIndex(b => b.id === afterBlock.id);
this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)];
this.updateValue();
if (focusNewBlock) {
setTimeout(() => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
if (wrapperElement && newBlock.type !== 'divider') {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement) {
blockElement.focus();
WysiwygBlocks.setCursorToStart(blockElement);
}
}
}, 50);
}
}
private async insertBlock(type: IBlock['type']) {
const currentBlockIndex = this.blocks.findIndex(b => b.id === this.selectedBlockId);
const currentBlock = this.blocks[currentBlockIndex];
if (currentBlock && currentBlock.content.startsWith('/')) {
if (currentBlock) {
// If it's a code block, ask for language
if (type === 'code') {
const language = await this.showLanguageSelectionModal();
@ -693,22 +613,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
if (type === 'divider') {
currentBlock.content = ' ';
const newBlock: IBlock = {
id: WysiwygShortcuts.generateBlockId(),
type: 'paragraph',
content: '',
};
this.blocks = [...this.blocks.slice(0, currentBlockIndex + 1), newBlock, ...this.blocks.slice(currentBlockIndex + 1)];
setTimeout(() => {
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
if (wrapperElement) {
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
if (blockElement) {
blockElement.focus();
}
}
});
const newBlock = this.createNewBlock();
this.insertBlockAfter(currentBlock, newBlock);
} else if (type === 'list') {
// Handle list type specially
currentBlock.metadata = { listType: 'bullet' }; // Default to bullet list
@ -982,13 +888,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
}
}
private getRootNodeOfNode(node: Node): Node {
let current: Node = node;
while (current.parentNode) {
current = current.parentNode;
}
return current;
}
private updateFormattingMenuPosition(): void {
console.log('updateFormattingMenuPosition called');

View File

@ -74,12 +74,13 @@ export class WysiwygBlocks {
console.log('Block mouseup event fired');
if (handlers.onMouseUp) handlers.onMouseUp(e);
}}"
.textContent="${block.content || ''}"
></div>
</div>
`;
}
return html`
const blockElement = html`
<div
class="block ${block.type} ${isSelected ? 'selected' : ''}"
contenteditable="true"
@ -93,8 +94,11 @@ export class WysiwygBlocks {
console.log('Block mouseup event fired');
if (handlers.onMouseUp) handlers.onMouseUp(e);
}}"
.innerHTML="${block.content || ''}"
></div>
`;
return blockElement;
}
static setCursorToEnd(element: HTMLElement): void {