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:
@ -162,9 +162,11 @@ export class DeesFormattingMenu extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public show(position: { x: number; y: number }, callback: (command: string) => void | Promise<void>): void {
|
public show(position: { x: number; y: number }, callback: (command: string) => void | Promise<void>): void {
|
||||||
|
console.log('FormattingMenu.show called:', { position, visible: this.visible });
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
|
console.log('FormattingMenu.show - visible set to:', this.visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
public hide(): void {
|
public hide(): void {
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
WysiwygDragDropHandler,
|
WysiwygDragDropHandler,
|
||||||
WysiwygModalManager,
|
WysiwygModalManager,
|
||||||
WysiwygHistory,
|
WysiwygHistory,
|
||||||
|
WysiwygSelection,
|
||||||
DeesSlashMenu,
|
DeesSlashMenu,
|
||||||
DeesFormattingMenu
|
DeesFormattingMenu
|
||||||
} from './index.js';
|
} from './index.js';
|
||||||
@ -138,11 +139,38 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
console.log('Adding selectionchange listener');
|
console.log('Adding selectionchange listener');
|
||||||
document.addEventListener('selectionchange', this.selectionChangeHandler);
|
document.addEventListener('selectionchange', this.selectionChangeHandler);
|
||||||
|
|
||||||
|
// Also add listener to our shadow root if supported
|
||||||
|
if (this.shadowRoot && 'addEventListener' in this.shadowRoot) {
|
||||||
|
this.shadowRoot.addEventListener('selectionchange', this.selectionChangeHandler);
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
if (!this.slashMenu.visible) {
|
||||||
this.selectedText = e.detail.text;
|
if (e.detail.hasSelection && e.detail.text.length > 0) {
|
||||||
this.updateFormattingMenuPosition();
|
this.selectedText = e.detail.text;
|
||||||
|
|
||||||
|
// Use the rect from the event if available
|
||||||
|
if (e.detail.rect) {
|
||||||
|
const coords = {
|
||||||
|
x: e.detail.rect.left + (e.detail.rect.width / 2),
|
||||||
|
y: Math.max(45, e.detail.rect.top - 45)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show the formatting menu at the calculated position
|
||||||
|
this.formattingMenu.show(
|
||||||
|
coords,
|
||||||
|
async (command: string) => await this.applyFormat(command)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.updateFormattingMenuPosition();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Clear selection
|
||||||
|
this.hideFormattingMenu();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -700,72 +728,157 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleSelectionChange(): void {
|
private handleSelectionChange(): void {
|
||||||
// Try to get selection from shadow root first, then fall back to window
|
console.log('=== handleSelectionChange called ===');
|
||||||
const shadowSelection = (this.shadowRoot as any).getSelection ? (this.shadowRoot as any).getSelection() : null;
|
|
||||||
const windowSelection = window.getSelection();
|
|
||||||
const editorContent = this.shadowRoot?.querySelector('.editor-content') as HTMLElement;
|
|
||||||
|
|
||||||
// Check both shadow and window selections
|
const selection = window.getSelection();
|
||||||
let selection = shadowSelection;
|
if (!selection || selection.rangeCount === 0) {
|
||||||
let selectedText = shadowSelection?.toString() || '';
|
if (this.formattingMenu.visible) {
|
||||||
|
console.log('No selection, hiding menu');
|
||||||
// If no shadow selection, check window selection
|
this.hideFormattingMenu();
|
||||||
if (!selectedText && windowSelection) {
|
}
|
||||||
selection = windowSelection;
|
|
||||||
selectedText = windowSelection.toString() || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Selection change:', {
|
|
||||||
hasText: selectedText.length > 0,
|
|
||||||
selectedText: selectedText.substring(0, 50),
|
|
||||||
shadowSelection: !!shadowSelection,
|
|
||||||
windowSelection: !!windowSelection,
|
|
||||||
rangeCount: selection?.rangeCount,
|
|
||||||
editorContent: !!editorContent
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!selection || selection.rangeCount === 0 || !editorContent) {
|
|
||||||
console.log('No selection or editor content');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have selected text, show the formatting menu
|
const selectedText = selection.toString().trim();
|
||||||
if (selectedText.length > 0) {
|
console.log('Selected text:', selectedText);
|
||||||
console.log('✅ Text selected:', selectedText);
|
|
||||||
|
|
||||||
if (selectedText !== this.selectedText) {
|
if (selectedText.length === 0) {
|
||||||
this.selectedText = selectedText;
|
if (this.formattingMenu.visible) {
|
||||||
this.updateFormattingMenuPosition();
|
console.log('No text selected, hiding menu');
|
||||||
|
this.hideFormattingMenu();
|
||||||
}
|
}
|
||||||
} else if (this.formattingMenu.visible) {
|
return;
|
||||||
console.log('No text selected, hiding menu');
|
}
|
||||||
|
|
||||||
|
// Get all shadow roots in our component tree
|
||||||
|
const shadowRoots: ShadowRoot[] = [];
|
||||||
|
if (this.shadowRoot) shadowRoots.push(this.shadowRoot);
|
||||||
|
|
||||||
|
// Find all block shadow roots
|
||||||
|
const blockWrappers = this.shadowRoot?.querySelectorAll('.block-wrapper');
|
||||||
|
console.log('Found block wrappers:', blockWrappers?.length || 0);
|
||||||
|
|
||||||
|
blockWrappers?.forEach(wrapper => {
|
||||||
|
const blockComponent = wrapper.querySelector('dees-wysiwyg-block');
|
||||||
|
if (blockComponent?.shadowRoot) {
|
||||||
|
shadowRoots.push(blockComponent.shadowRoot);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Shadow roots collected:', shadowRoots.length);
|
||||||
|
|
||||||
|
// Try using getComposedRanges if available
|
||||||
|
let ranges: Range[] | StaticRange[] = [];
|
||||||
|
let selectionInEditor = false;
|
||||||
|
|
||||||
|
if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function') {
|
||||||
|
console.log('Using getComposedRanges with shadow roots');
|
||||||
|
try {
|
||||||
|
// According to MDN, pass shadow roots in options object
|
||||||
|
ranges = selection.getComposedRanges({ shadowRoots });
|
||||||
|
console.log('getComposedRanges returned', ranges.length, 'ranges');
|
||||||
|
|
||||||
|
if (ranges.length > 0) {
|
||||||
|
const range = ranges[0];
|
||||||
|
// Check if the range is within our editor
|
||||||
|
selectionInEditor = this.isRangeInEditor(range);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('getComposedRanges failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to regular selection API
|
||||||
|
if (ranges.length === 0 && selection.rangeCount > 0) {
|
||||||
|
console.log('Falling back to getRangeAt');
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
ranges = [range];
|
||||||
|
selectionInEditor = this.isRangeInEditor(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Selection in editor:', selectionInEditor);
|
||||||
|
|
||||||
|
if (selectionInEditor && selectedText !== this.selectedText) {
|
||||||
|
console.log('✅ Text selected in editor:', selectedText);
|
||||||
|
this.selectedText = selectedText;
|
||||||
|
this.updateFormattingMenuPosition();
|
||||||
|
} else if (!selectionInEditor && this.formattingMenu.visible) {
|
||||||
|
console.log('Selection not in editor, hiding menu');
|
||||||
this.hideFormattingMenu();
|
this.hideFormattingMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isRangeInEditor(range: Range | StaticRange): boolean {
|
||||||
|
// Check if the selection is within one of our blocks
|
||||||
|
const blockWrappers = this.shadowRoot?.querySelectorAll('.block-wrapper');
|
||||||
|
if (!blockWrappers) return false;
|
||||||
|
|
||||||
|
// Check each block
|
||||||
|
for (let i = 0; i < blockWrappers.length; i++) {
|
||||||
|
const wrapper = blockWrappers[i];
|
||||||
|
const blockComponent = wrapper.querySelector('dees-wysiwyg-block');
|
||||||
|
if (blockComponent?.shadowRoot) {
|
||||||
|
const editableElements = blockComponent.shadowRoot.querySelectorAll('.block, .block.code');
|
||||||
|
for (let j = 0; j < editableElements.length; j++) {
|
||||||
|
const elem = editableElements[j];
|
||||||
|
if (elem) {
|
||||||
|
// For StaticRange, we need to check differently
|
||||||
|
if ('startContainer' in range) {
|
||||||
|
// Check if the range nodes are within this element
|
||||||
|
const startInElement = this.isNodeInElement(range.startContainer, elem);
|
||||||
|
const endInElement = this.isNodeInElement(range.endContainer, elem);
|
||||||
|
|
||||||
|
if (startInElement || endInElement) {
|
||||||
|
console.log('Selection found in block:', wrapper.getAttribute('data-block-id'));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Selection not in any block. Range:', range);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a node is within an element (handles shadow DOM)
|
||||||
|
*/
|
||||||
|
private isNodeInElement(node: Node, element: Element): boolean {
|
||||||
|
let current: Node | null = node;
|
||||||
|
while (current) {
|
||||||
|
if (current === element) return true;
|
||||||
|
// Walk up the tree, including shadow host if in shadow DOM
|
||||||
|
current = current.parentNode || (current as any).host;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private updateFormattingMenuPosition(): void {
|
private updateFormattingMenuPosition(): void {
|
||||||
console.log('updateFormattingMenuPosition called');
|
console.log('updateFormattingMenuPosition called');
|
||||||
const coords = WysiwygFormatting.getSelectionCoordinates(this.shadowRoot as ShadowRoot);
|
|
||||||
|
// Get all shadow roots
|
||||||
|
const shadowRoots: ShadowRoot[] = [];
|
||||||
|
if (this.shadowRoot) shadowRoots.push(this.shadowRoot);
|
||||||
|
|
||||||
|
// Find all block shadow roots
|
||||||
|
const blockWrappers = this.shadowRoot?.querySelectorAll('.block-wrapper');
|
||||||
|
blockWrappers?.forEach(wrapper => {
|
||||||
|
const blockComponent = wrapper.querySelector('dees-wysiwyg-block');
|
||||||
|
if (blockComponent?.shadowRoot) {
|
||||||
|
shadowRoots.push(blockComponent.shadowRoot);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const coords = WysiwygFormatting.getSelectionCoordinates(...shadowRoots);
|
||||||
console.log('Selection coordinates:', coords);
|
console.log('Selection coordinates:', coords);
|
||||||
|
|
||||||
if (coords) {
|
if (coords) {
|
||||||
const container = this.shadowRoot!.querySelector('.wysiwyg-container');
|
// Show the global formatting menu at absolute coordinates
|
||||||
if (!container) {
|
|
||||||
console.error('Container not found!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerRect = container.getBoundingClientRect();
|
|
||||||
const formattingMenuPosition = {
|
|
||||||
x: coords.x - containerRect.left,
|
|
||||||
y: coords.y - containerRect.top
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Setting menu position:', formattingMenuPosition);
|
|
||||||
// Show the global formatting menu
|
|
||||||
this.formattingMenu.show(
|
this.formattingMenu.show(
|
||||||
{ x: coords.x, y: coords.y }, // Use absolute coordinates
|
{ x: coords.x, y: coords.y },
|
||||||
async (command: string) => await this.applyFormat(command)
|
async (command: string) => await this.applyFormat(command)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -779,33 +892,56 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async applyFormat(command: string): Promise<void> {
|
public async applyFormat(command: string): Promise<void> {
|
||||||
// Save current selection before applying format
|
// Get all shadow roots
|
||||||
const selection = window.getSelection();
|
const shadowRoots: ShadowRoot[] = [];
|
||||||
if (!selection || selection.rangeCount === 0) return;
|
if (this.shadowRoot) shadowRoots.push(this.shadowRoot);
|
||||||
|
|
||||||
// Get the current block
|
// Find all block shadow roots
|
||||||
const anchorNode = selection.anchorNode;
|
const blockWrappers = this.shadowRoot?.querySelectorAll('.block-wrapper');
|
||||||
const blockElement = anchorNode?.nodeType === Node.TEXT_NODE
|
blockWrappers?.forEach(wrapper => {
|
||||||
? anchorNode.parentElement?.closest('.block')
|
const blockComponent = wrapper.querySelector('dees-wysiwyg-block');
|
||||||
: (anchorNode as Element)?.closest('.block');
|
if (blockComponent?.shadowRoot) {
|
||||||
|
shadowRoots.push(blockComponent.shadowRoot);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!blockElement) return;
|
// Get selection info using Shadow DOM-aware utilities
|
||||||
|
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||||
|
if (!selectionInfo) return;
|
||||||
|
|
||||||
const blockWrapper = blockElement.closest('.block-wrapper');
|
// Find which block contains the selection
|
||||||
const blockId = blockWrapper?.getAttribute('data-block-id');
|
let targetBlock: IBlock | undefined;
|
||||||
const block = this.blocks.find(b => b.id === blockId);
|
let targetBlockComponent: any;
|
||||||
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as any;
|
|
||||||
|
|
||||||
if (!block || !blockComponent) return;
|
const wrappers = this.shadowRoot!.querySelectorAll('.block-wrapper');
|
||||||
|
for (let i = 0; i < wrappers.length; i++) {
|
||||||
|
const wrapper = wrappers[i];
|
||||||
|
const blockComponent = wrapper.querySelector('dees-wysiwyg-block') as any;
|
||||||
|
if (blockComponent?.shadowRoot) {
|
||||||
|
const block = blockComponent.shadowRoot.querySelector('.block');
|
||||||
|
if (block && (
|
||||||
|
block.contains(selectionInfo.startContainer) ||
|
||||||
|
block.contains(selectionInfo.endContainer)
|
||||||
|
)) {
|
||||||
|
const blockId = wrapper.getAttribute('data-block-id');
|
||||||
|
targetBlock = this.blocks.find(b => b.id === blockId);
|
||||||
|
targetBlockComponent = blockComponent;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetBlock || !targetBlockComponent) return;
|
||||||
|
|
||||||
// Handle link command specially
|
// Handle link command specially
|
||||||
if (command === 'link') {
|
if (command === 'link') {
|
||||||
const url = await this.showLinkDialog();
|
const url = await this.showLinkDialog();
|
||||||
if (!url) {
|
if (!url) {
|
||||||
// User cancelled - restore focus to block
|
// User cancelled - restore focus to block
|
||||||
blockComponent.focus();
|
targetBlockComponent.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Apply link format
|
||||||
WysiwygFormatting.applyFormat(command, url);
|
WysiwygFormatting.applyFormat(command, url);
|
||||||
} else {
|
} else {
|
||||||
// Apply the format
|
// Apply the format
|
||||||
@ -813,10 +949,10 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update content after a microtask to ensure DOM is updated
|
// Update content after a microtask to ensure DOM is updated
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
|
||||||
// Force content update
|
// Force content update
|
||||||
block.content = blockComponent.getContent();
|
targetBlock.content = targetBlockComponent.getContent();
|
||||||
|
|
||||||
// Update value to persist changes
|
// Update value to persist changes
|
||||||
this.updateValue();
|
this.updateValue();
|
||||||
@ -825,14 +961,9 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
if (command === 'link') {
|
if (command === 'link') {
|
||||||
this.hideFormattingMenu();
|
this.hideFormattingMenu();
|
||||||
} else if (this.formattingMenu.visible) {
|
} else if (this.formattingMenu.visible) {
|
||||||
// Update menu position if still showing
|
// Keep selection and update menu position
|
||||||
this.updateFormattingMenuPosition();
|
this.updateFormattingMenuPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure block still has focus
|
|
||||||
if (document.activeElement !== blockElement) {
|
|
||||||
blockComponent.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async showLinkDialog(): Promise<string | null> {
|
private async showLinkDialog(): Promise<string | null> {
|
||||||
|
@ -315,6 +315,13 @@ export class DeesWysiwygBlock extends DeesElement {
|
|||||||
if (pos !== null) {
|
if (pos !== null) {
|
||||||
this.lastKnownCursorPosition = pos;
|
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', () => {
|
editableBlock.addEventListener('focus', () => {
|
||||||
@ -341,6 +348,9 @@ export class DeesWysiwygBlock extends DeesElement {
|
|||||||
this.lastKnownCursorPosition = pos;
|
this.lastKnownCursorPosition = pos;
|
||||||
console.log('Cursor position after mouseup:', pos);
|
console.log('Cursor position after mouseup:', pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for text selection
|
||||||
|
this.checkForTextSelection();
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
this.handleMouseUp(e);
|
this.handleMouseUp(e);
|
||||||
@ -778,32 +788,75 @@ export class DeesWysiwygBlock extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleMouseUp(_e: MouseEvent): void {
|
private handleMouseUp(_e: MouseEvent): void {
|
||||||
// Check if we have a selection within this block
|
// Selection check is now handled in the mouseup event listener
|
||||||
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'
|
* Check if there's text selected within this block
|
||||||
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
|
*/
|
||||||
: this.shadowRoot?.querySelector('.block') as HTMLDivElement;
|
private checkForTextSelection(): void {
|
||||||
if (editableElement && editableElement.contains(range.commonAncestorContainer)) {
|
const editableElement = this.block?.type === 'code'
|
||||||
const selectedText = selection.toString();
|
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
|
||||||
if (selectedText.length > 0) {
|
: this.shadowRoot?.querySelector('.block') as HTMLDivElement;
|
||||||
// Dispatch a custom event that can cross shadow DOM boundaries
|
if (!editableElement) return;
|
||||||
this.dispatchEvent(new CustomEvent('block-text-selected', {
|
|
||||||
detail: {
|
const selection = window.getSelection();
|
||||||
text: selectedText,
|
if (!selection || selection.rangeCount === 0) {
|
||||||
blockId: this.block.id,
|
// Dispatch event to clear selection
|
||||||
range: range
|
this.dispatchEvent(new CustomEvent('block-text-selected', {
|
||||||
},
|
detail: {
|
||||||
bubbles: true,
|
text: '',
|
||||||
composed: true
|
blockId: this.block.id,
|
||||||
}));
|
hasSelection: false
|
||||||
}
|
},
|
||||||
}
|
bubbles: true,
|
||||||
}
|
composed: true
|
||||||
}, 10);
|
}));
|
||||||
|
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
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -143,11 +143,9 @@ export class WysiwygFormatting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getSelectionCoordinates(shadowRoot?: ShadowRoot): { x: number, y: number } | null {
|
static getSelectionCoordinates(...shadowRoots: ShadowRoot[]): { x: number, y: number } | null {
|
||||||
// Get selection info using the new utility that handles Shadow DOM
|
// Get selection info using the new utility that handles Shadow DOM
|
||||||
const selectionInfo = shadowRoot
|
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||||
? WysiwygSelection.getSelectionInfo(shadowRoot)
|
|
||||||
: WysiwygSelection.getSelectionInfo();
|
|
||||||
|
|
||||||
console.log('getSelectionCoordinates - selectionInfo:', selectionInfo);
|
console.log('getSelectionCoordinates - selectionInfo:', selectionInfo);
|
||||||
|
|
||||||
@ -162,8 +160,29 @@ export class WysiwygFormatting {
|
|||||||
|
|
||||||
console.log('Range rect:', rect);
|
console.log('Range rect:', rect);
|
||||||
|
|
||||||
if (rect.width === 0) {
|
if (rect.width === 0 && rect.height === 0) {
|
||||||
console.log('Rect width is 0');
|
console.log('Rect width and height are 0, trying different approach');
|
||||||
|
// Sometimes the rect is collapsed, let's try getting the caret position
|
||||||
|
if ('caretPositionFromPoint' in document) {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection && selection.rangeCount > 0) {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
const tempSpan = document.createElement('span');
|
||||||
|
tempSpan.textContent = '\u200B'; // Zero-width space
|
||||||
|
range.insertNode(tempSpan);
|
||||||
|
const spanRect = tempSpan.getBoundingClientRect();
|
||||||
|
tempSpan.remove();
|
||||||
|
|
||||||
|
if (spanRect.width > 0 || spanRect.height > 0) {
|
||||||
|
const coords = {
|
||||||
|
x: spanRect.left,
|
||||||
|
y: Math.max(45, spanRect.top - 45)
|
||||||
|
};
|
||||||
|
console.log('Used span trick for coords:', coords);
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,13 +20,16 @@ export class WysiwygSelection {
|
|||||||
*/
|
*/
|
||||||
static getSelectionInfo(...shadowRoots: ShadowRoot[]): SelectionInfo | null {
|
static getSelectionInfo(...shadowRoots: ShadowRoot[]): SelectionInfo | null {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
|
console.log('WysiwygSelection.getSelectionInfo - selection:', selection, 'rangeCount:', selection?.rangeCount);
|
||||||
if (!selection) return null;
|
if (!selection) return null;
|
||||||
|
|
||||||
// Try using getComposedRanges if available (better Shadow DOM support)
|
// Try using getComposedRanges if available (better Shadow DOM support)
|
||||||
if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function') {
|
if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function') {
|
||||||
|
console.log('Using getComposedRanges with', shadowRoots.length, 'shadow roots');
|
||||||
try {
|
try {
|
||||||
// Pass shadow roots in the correct format as per MDN
|
// Pass shadow roots in the correct format as per MDN
|
||||||
const ranges = selection.getComposedRanges({ shadowRoots });
|
const ranges = selection.getComposedRanges({ shadowRoots });
|
||||||
|
console.log('getComposedRanges returned', ranges.length, 'ranges');
|
||||||
if (ranges.length > 0) {
|
if (ranges.length > 0) {
|
||||||
const range = ranges[0];
|
const range = ranges[0];
|
||||||
return {
|
return {
|
||||||
@ -40,6 +43,8 @@ export class WysiwygSelection {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('getComposedRanges failed, falling back to getRangeAt:', error);
|
console.warn('getComposedRanges failed, falling back to getRangeAt:', error);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log('getComposedRanges not available, using fallback');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to traditional selection API
|
// Fallback to traditional selection API
|
||||||
|
Reference in New Issue
Block a user