Improve Wysiwyg editor
This commit is contained in:
@ -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,14 +472,17 @@ 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) {
|
||||
// Clear the slash command text
|
||||
blockElement.textContent = '';
|
||||
currentBlock.content = '';
|
||||
// Ensure cursor stays in the block
|
||||
blockElement.focus();
|
||||
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 = '';
|
||||
// Ensure cursor stays in the block
|
||||
blockElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user