fix(wysiwyg): Fix text selection detection for formatting menu in Shadow DOM

- Update selection detection to properly handle Shadow DOM boundaries
- Use getComposedRanges API correctly according to MDN documentation
- Add direct selection detection within block components
- Dispatch custom events from blocks when text is selected
- Fix formatting menu positioning using selection rect from events
This commit is contained in:
Juergen Kunz
2025-06-24 16:17:00 +00:00
parent ca525ce7e3
commit 3b93bd63a7
5 changed files with 320 additions and 110 deletions

View File

@ -315,6 +315,13 @@ export class DeesWysiwygBlock extends DeesElement {
if (pos !== null) {
this.lastKnownCursorPosition = pos;
}
// Check for selection after keyboard navigation
if (e.shiftKey || ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
setTimeout(() => {
this.checkForTextSelection();
}, 10);
}
});
editableBlock.addEventListener('focus', () => {
@ -341,6 +348,9 @@ export class DeesWysiwygBlock extends DeesElement {
this.lastKnownCursorPosition = pos;
console.log('Cursor position after mouseup:', pos);
}
// Check for text selection
this.checkForTextSelection();
}, 0);
this.handleMouseUp(e);
@ -778,32 +788,75 @@ export class DeesWysiwygBlock extends DeesElement {
}
private handleMouseUp(_e: MouseEvent): void {
// Check if we have a selection within this block
setTimeout(() => {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
// Check if selection is within this block
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.shadowRoot?.querySelector('.block') as HTMLDivElement;
if (editableElement && editableElement.contains(range.commonAncestorContainer)) {
const selectedText = selection.toString();
if (selectedText.length > 0) {
// Dispatch a custom event that can cross shadow DOM boundaries
this.dispatchEvent(new CustomEvent('block-text-selected', {
detail: {
text: selectedText,
blockId: this.block.id,
range: range
},
bubbles: true,
composed: true
}));
}
}
}
}, 10);
// Selection check is now handled in the mouseup event listener
}
/**
* Check if there's text selected within this block
*/
private checkForTextSelection(): void {
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.shadowRoot?.querySelector('.block') as HTMLDivElement;
if (!editableElement) return;
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
// Dispatch event to clear selection
this.dispatchEvent(new CustomEvent('block-text-selected', {
detail: {
text: '',
blockId: this.block.id,
hasSelection: false
},
bubbles: true,
composed: true
}));
return;
}
const selectedText = selection.toString().trim();
// Only proceed if we have selected text
if (selectedText.length === 0) {
// Dispatch event to clear selection
this.dispatchEvent(new CustomEvent('block-text-selected', {
detail: {
text: '',
blockId: this.block.id,
hasSelection: false
},
bubbles: true,
composed: true
}));
return;
}
// Check if the selection is within this block
const range = selection.getRangeAt(0);
// Check if both start and end are within our editable element
const startInBlock = editableElement.contains(range.startContainer);
const endInBlock = editableElement.contains(range.endContainer);
if (startInBlock && endInBlock) {
console.log('Block detected text selection:', selectedText);
// Get the bounding rect of the selection
const rect = range.getBoundingClientRect();
// Dispatch event to parent with selection details
this.dispatchEvent(new CustomEvent('block-text-selected', {
detail: {
text: selectedText,
blockId: this.block.id,
range: range,
rect: rect,
hasSelection: true
},
bubbles: true,
composed: true
}));
}
}
}