Improve Wysiwyg editor
This commit is contained in:
@ -108,51 +108,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
// Add global selection listener
|
// Add global selection listener
|
||||||
console.log('Adding selectionchange listener');
|
console.log('Adding selectionchange listener');
|
||||||
document.addEventListener('selectionchange', this.selectionChangeHandler);
|
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 {
|
render(): TemplateResult {
|
||||||
@ -276,12 +231,17 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
if (listElement) {
|
if (listElement) {
|
||||||
block.metadata = { listType: listElement.tagName.toLowerCase() === 'ol' ? 'ordered' : 'bullet' };
|
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 || '';
|
block.content = target.textContent || '';
|
||||||
|
} else {
|
||||||
|
// For other blocks, preserve HTML formatting
|
||||||
|
block.content = target.innerHTML || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for block type change intents
|
// Check for block type change intents (use text content for detection, not HTML)
|
||||||
const detectedType = this.detectBlockTypeIntent(block.content);
|
const textContent = target.textContent || '';
|
||||||
|
const detectedType = this.detectBlockTypeIntent(textContent);
|
||||||
|
|
||||||
// Only process if the detected type is different from current type
|
// Only process if the detected type is different from current type
|
||||||
if (detectedType && detectedType.type !== block.type) {
|
if (detectedType && detectedType.type !== block.type) {
|
||||||
@ -311,23 +271,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
block.content = ' ';
|
block.content = ' ';
|
||||||
|
|
||||||
// Create a new paragraph block after the divider
|
// Create a new paragraph block after the divider
|
||||||
const blockIndex = this.blocks.findIndex(b => b.id === block.id);
|
const newBlock = this.createNewBlock();
|
||||||
const newBlock: IBlock = {
|
this.insertBlockAfter(block, newBlock);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateValue();
|
this.updateValue();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
@ -366,9 +311,9 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for slash commands at the beginning of any block
|
// 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
|
// Only show menu on initial '/', or update filter if already showing
|
||||||
if (!this.showSlashMenu && block.content === '/') {
|
if (!this.showSlashMenu && textContent === '/') {
|
||||||
this.showSlashMenu = true;
|
this.showSlashMenu = true;
|
||||||
this.slashMenuSelectedIndex = 0;
|
this.slashMenuSelectedIndex = 0;
|
||||||
const rect = target.getBoundingClientRect();
|
const rect = target.getBoundingClientRect();
|
||||||
@ -378,8 +323,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
y: rect.bottom - containerRect.top + 4
|
y: rect.bottom - containerRect.top + 4
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.slashMenuFilter = block.content.slice(1);
|
this.slashMenuFilter = textContent.slice(1);
|
||||||
} else if (!block.content.startsWith('/')) {
|
} else if (!textContent.startsWith('/')) {
|
||||||
this.closeSlashMenu();
|
this.closeSlashMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,28 +386,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
// Shift+Enter in code blocks creates a new block
|
// Shift+Enter in code blocks creates a new block
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const newBlock = this.createNewBlock();
|
||||||
const blockIndex = this.blocks.findIndex(b => b.id === block.id);
|
this.insertBlockAfter(block, newBlock);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// For normal Enter in code blocks, let the browser handle it (creates new line)
|
// For normal Enter in code blocks, let the browser handle it (creates new line)
|
||||||
return;
|
return;
|
||||||
@ -481,25 +406,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
if (currentLi && currentLi.textContent === '') {
|
if (currentLi && currentLi.textContent === '') {
|
||||||
// Empty list item - exit list mode
|
// Empty list item - exit list mode
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const blockIndex = this.blocks.findIndex(b => b.id === block.id);
|
const newBlock = this.createNewBlock();
|
||||||
const newBlock: IBlock = {
|
this.insertBlockAfter(block, newBlock);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// Otherwise, let the browser handle creating new list items
|
// Otherwise, let the browser handle creating new list items
|
||||||
}
|
}
|
||||||
@ -507,28 +415,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const newBlock = this.createNewBlock();
|
||||||
const blockIndex = this.blocks.findIndex(b => b.id === block.id);
|
this.insertBlockAfter(block, newBlock);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (e.key === 'Backspace' && block.content === '' && this.blocks.length > 1) {
|
} else if (e.key === 'Backspace' && block.content === '' && this.blocks.length > 1) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -584,14 +472,17 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
if (this.showSlashMenu && this.selectedBlockId) {
|
if (this.showSlashMenu && this.selectedBlockId) {
|
||||||
// Clear the slash command from the content if menu is closing without selection
|
// Clear the slash command from the content if menu is closing without selection
|
||||||
const currentBlock = this.blocks.find(b => b.id === this.selectedBlockId);
|
const currentBlock = this.blocks.find(b => b.id === this.selectedBlockId);
|
||||||
if (currentBlock && currentBlock.content.startsWith('/')) {
|
if (currentBlock) {
|
||||||
const blockElement = this.shadowRoot!.querySelector(`[data-block-id="${currentBlock.id}"]`) as HTMLDivElement;
|
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${currentBlock.id}"]`);
|
||||||
if (blockElement) {
|
if (wrapperElement) {
|
||||||
// Clear the slash command text
|
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
|
||||||
blockElement.textContent = '';
|
if (blockElement && (blockElement.textContent || '').startsWith('/')) {
|
||||||
currentBlock.content = '';
|
// Clear the slash command text
|
||||||
// Ensure cursor stays in the block
|
blockElement.textContent = '';
|
||||||
blockElement.focus();
|
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']) {
|
private async insertBlock(type: IBlock['type']) {
|
||||||
const currentBlockIndex = this.blocks.findIndex(b => b.id === this.selectedBlockId);
|
const currentBlockIndex = this.blocks.findIndex(b => b.id === this.selectedBlockId);
|
||||||
const currentBlock = this.blocks[currentBlockIndex];
|
const currentBlock = this.blocks[currentBlockIndex];
|
||||||
|
|
||||||
if (currentBlock && currentBlock.content.startsWith('/')) {
|
if (currentBlock) {
|
||||||
// If it's a code block, ask for language
|
// If it's a code block, ask for language
|
||||||
if (type === 'code') {
|
if (type === 'code') {
|
||||||
const language = await this.showLanguageSelectionModal();
|
const language = await this.showLanguageSelectionModal();
|
||||||
@ -693,22 +613,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
|
|
||||||
if (type === 'divider') {
|
if (type === 'divider') {
|
||||||
currentBlock.content = ' ';
|
currentBlock.content = ' ';
|
||||||
const newBlock: IBlock = {
|
const newBlock = this.createNewBlock();
|
||||||
id: WysiwygShortcuts.generateBlockId(),
|
this.insertBlockAfter(currentBlock, newBlock);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'list') {
|
} else if (type === 'list') {
|
||||||
// Handle list type specially
|
// Handle list type specially
|
||||||
currentBlock.metadata = { listType: 'bullet' }; // Default to bullet list
|
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 {
|
private updateFormattingMenuPosition(): void {
|
||||||
console.log('updateFormattingMenuPosition called');
|
console.log('updateFormattingMenuPosition called');
|
||||||
|
@ -74,12 +74,13 @@ export class WysiwygBlocks {
|
|||||||
console.log('Block mouseup event fired');
|
console.log('Block mouseup event fired');
|
||||||
if (handlers.onMouseUp) handlers.onMouseUp(e);
|
if (handlers.onMouseUp) handlers.onMouseUp(e);
|
||||||
}}"
|
}}"
|
||||||
|
.textContent="${block.content || ''}"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
const blockElement = html`
|
||||||
<div
|
<div
|
||||||
class="block ${block.type} ${isSelected ? 'selected' : ''}"
|
class="block ${block.type} ${isSelected ? 'selected' : ''}"
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
@ -93,8 +94,11 @@ export class WysiwygBlocks {
|
|||||||
console.log('Block mouseup event fired');
|
console.log('Block mouseup event fired');
|
||||||
if (handlers.onMouseUp) handlers.onMouseUp(e);
|
if (handlers.onMouseUp) handlers.onMouseUp(e);
|
||||||
}}"
|
}}"
|
||||||
|
.innerHTML="${block.content || ''}"
|
||||||
></div>
|
></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
return blockElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
static setCursorToEnd(element: HTMLElement): void {
|
static setCursorToEnd(element: HTMLElement): void {
|
||||||
|
Reference in New Issue
Block a user