diff --git a/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts b/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts
index eba814f..80256da 100644
--- a/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts
+++ b/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts
@@ -6,7 +6,6 @@ import {
type TemplateResult,
cssManager,
css,
- query,
} from '@design.estate/dees-element';
import { type IBlock } from './wysiwyg.types.js';
@@ -38,11 +37,14 @@ export class DeesWysiwygBlock extends DeesElement {
onMouseUp?: (e: MouseEvent) => void;
};
- @query('.block')
- private blockElement: HTMLDivElement;
+ // Reference to the editable block element
+ private blockElement: HTMLDivElement | null = null;
// Track if we've initialized the content
private contentInitialized: boolean = false;
+
+ // Track cursor position
+ private lastKnownCursorPosition: number = 0;
public static styles = [
cssManager.defaultStyles,
@@ -270,10 +272,16 @@ export class DeesWysiwygBlock extends DeesElement {
// Mark that content has been initialized
this.contentInitialized = true;
- // For code blocks, the actual contenteditable block is nested
+ // First, populate the container with the rendered content
+ const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
+ if (container && this.block) {
+ container.innerHTML = this.renderBlockContent();
+ }
+
+ // Now find the actual editable block element
const editableBlock = this.block.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
- : this.blockElement;
+ : this.shadowRoot?.querySelector('.block') as HTMLDivElement;
// Ensure the block element maintains its content
if (editableBlock) {
@@ -282,16 +290,31 @@ export class DeesWysiwygBlock extends DeesElement {
// Set up all event handlers manually to avoid Lit re-renders
editableBlock.addEventListener('input', (e) => {
- this.logCursorPosition('input');
this.handlers?.onInput?.(e as InputEvent);
+
+ // Track cursor position after input
+ const pos = this.getCursorPosition(editableBlock);
+ if (pos !== null) {
+ this.lastKnownCursorPosition = pos;
+ }
});
editableBlock.addEventListener('keydown', (e) => {
+ // Track cursor position before keydown
+ const pos = this.getCursorPosition(editableBlock);
+ if (pos !== null) {
+ this.lastKnownCursorPosition = pos;
+ }
+
this.handlers?.onKeyDown?.(e);
});
editableBlock.addEventListener('keyup', (e) => {
- this.logCursorPosition('keyup', e);
+ // Track cursor position after key release
+ const pos = this.getCursorPosition(editableBlock);
+ if (pos !== null) {
+ this.lastKnownCursorPosition = pos;
+ }
});
editableBlock.addEventListener('focus', () => {
@@ -311,13 +334,28 @@ export class DeesWysiwygBlock extends DeesElement {
});
editableBlock.addEventListener('mouseup', (e) => {
- this.logCursorPosition('mouseup');
+ // Small delay to let browser set cursor position
+ setTimeout(() => {
+ const pos = this.getCursorPosition(editableBlock);
+ if (pos !== null) {
+ this.lastKnownCursorPosition = pos;
+ console.log('Cursor position after mouseup:', pos);
+ }
+ }, 0);
+
this.handleMouseUp(e);
this.handlers?.onMouseUp?.(e);
});
- editableBlock.addEventListener('click', () => {
- this.logCursorPosition('click');
+ editableBlock.addEventListener('click', (e: MouseEvent) => {
+ // Small delay to let browser set cursor position
+ setTimeout(() => {
+ const pos = this.getCursorPosition(editableBlock);
+ if (pos !== null) {
+ this.lastKnownCursorPosition = pos;
+ console.log('Cursor position after click:', pos);
+ }
+ }, 0);
});
// Set initial content if needed
@@ -332,15 +370,23 @@ export class DeesWysiwygBlock extends DeesElement {
}
}
- // For code blocks, we use the nested editableBlock
- // The blockElement getter will automatically find the right element
+ // Store reference to the block element for quick access
+ this.blockElement = editableBlock;
}
render(): TemplateResult {
if (!this.block) return html``;
+ // Since we need dynamic content, we'll render an empty container
+ // and set the innerHTML in firstUpdated
+ return html`
${language}
@@ -362,11 +409,10 @@ export class DeesWysiwygBlock extends DeesElement {
}
const placeholder = this.getPlaceholder();
-
- // Return static HTML without event bindings
- return html`
+ const selectedClass = this.isSelected ? ' selected' : '';
+ return `
@@ -395,7 +441,7 @@ export class DeesWysiwygBlock extends DeesElement {
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
- : this.blockElement;
+ : this.shadowRoot?.querySelector('.block') as HTMLDivElement;
if (!editableElement) return;
@@ -418,7 +464,7 @@ export class DeesWysiwygBlock extends DeesElement {
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
- : this.blockElement;
+ : this.shadowRoot?.querySelector('.block') as HTMLDivElement;
if (!editableElement) return;
@@ -455,24 +501,66 @@ export class DeesWysiwygBlock extends DeesElement {
}
}
- private getFirstTextNode(node: Node): Text | null {
- if (node.nodeType === Node.TEXT_NODE) {
- return node as Text;
+
+
+ /**
+ * Get cursor position in the editable element
+ */
+ private getCursorPosition(element: HTMLElement): number | null {
+ // Get parent wysiwyg component's shadow root
+ const parentComponent = this.closest('dees-input-wysiwyg');
+ const parentShadowRoot = parentComponent?.shadowRoot;
+
+ // Get selection info with both shadow roots for proper traversal
+ const shadowRoots: ShadowRoot[] = [];
+ if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
+ if (this.shadowRoot) shadowRoots.push(this.shadowRoot);
+
+ const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
+ console.log('getCursorPosition: Selection info from shadow DOMs:', {
+ selectionInfo,
+ shadowRootsCount: shadowRoots.length
+ });
+
+ if (!selectionInfo) {
+ console.log('getCursorPosition: No selection found');
+ return null;
}
- for (let i = 0; i < node.childNodes.length; i++) {
- const textNode = this.getFirstTextNode(node.childNodes[i]);
- if (textNode) return textNode;
+ console.log('getCursorPosition: Range info:', {
+ startContainer: selectionInfo.startContainer,
+ startOffset: selectionInfo.startOffset,
+ collapsed: selectionInfo.collapsed,
+ startContainerText: selectionInfo.startContainer.textContent
+ });
+
+ if (!element.contains(selectionInfo.startContainer)) {
+ console.log('getCursorPosition: Range not in element');
+ return null;
}
- return null;
+ // Create a range from start of element to cursor position
+ const preCaretRange = document.createRange();
+ preCaretRange.selectNodeContents(element);
+ preCaretRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
+
+ // Get the text content length up to cursor
+ const position = preCaretRange.toString().length;
+ console.log('getCursorPosition: Calculated position:', {
+ position,
+ preCaretText: preCaretRange.toString(),
+ elementText: element.textContent,
+ elementTextLength: element.textContent?.length
+ });
+
+ return position;
}
public getContent(): string {
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
- : this.blockElement;
+ : this.shadowRoot?.querySelector('.block') as HTMLDivElement;
if (!editableElement) return '';
@@ -490,7 +578,7 @@ export class DeesWysiwygBlock extends DeesElement {
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
- : this.blockElement;
+ : this.shadowRoot?.querySelector('.block') as HTMLDivElement;
if (!editableElement) return;
@@ -531,7 +619,10 @@ export class DeesWysiwygBlock extends DeesElement {
public focusListItem(): void {
if (this.block.type === 'list') {
- WysiwygBlocks.focusListItem(this.blockElement);
+ const editableElement = this.shadowRoot?.querySelector('.block') as HTMLDivElement;
+ if (editableElement) {
+ WysiwygBlocks.focusListItem(editableElement);
+ }
}
}
@@ -539,179 +630,149 @@ export class DeesWysiwygBlock extends DeesElement {
* Gets content split at cursor position
*/
public getSplitContent(): { before: string; after: string } | null {
- if (!this.blockElement) return null;
+ console.log('getSplitContent: Starting...');
- // Get the full content first
- const fullContent = this.getContent();
- console.log('getSplitContent: Full content:', {
- content: fullContent,
- length: fullContent.length,
- blockType: this.block.type
- });
-
- // Direct approach: Get selection from window
- const selection = window.getSelection();
- if (!selection || selection.rangeCount === 0) {
- console.log('getSplitContent: No selection found');
- return {
- before: fullContent,
- after: ''
- };
- }
-
- const range = selection.getRangeAt(0);
- console.log('getSplitContent: Range info:', {
- startContainer: range.startContainer,
- startOffset: range.startOffset,
- collapsed: range.collapsed,
- startContainerType: range.startContainer.nodeType,
- startContainerText: range.startContainer.textContent?.substring(0, 50)
- });
-
- // Check if this block element has focus or contains the selection
- const activeElement = this.shadowRoot?.activeElement || document.activeElement;
- const hasFocus = this.blockElement === activeElement || this.blockElement?.contains(activeElement as Node);
-
- // For contenteditable, check if selection is in our shadow DOM
- let selectionInThisBlock = false;
- try {
- // Walk up from the selection to see if we reach our block element
- let node: Node | null = range.startContainer;
- while (node) {
- if (node === this.blockElement || node === this.shadowRoot) {
- selectionInThisBlock = true;
- break;
- }
- node = node.parentNode || (node as any).host; // Check shadow host too
- }
- } catch (e) {
- console.log('Error checking selection ancestry:', e);
- }
-
- console.log('getSplitContent: Focus check:', {
- hasFocus,
- selectionInThisBlock,
- activeElement,
- blockElement: this.blockElement
- });
-
- if (!hasFocus && !selectionInThisBlock) {
- console.log('getSplitContent: Block does not have focus/selection');
- return null;
- }
-
- // Get the actual editable element (might be nested for code blocks)
- const editableElement = this.block.type === 'code'
+ // Get the actual editable element first
+ const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
- : this.blockElement;
+ : this.shadowRoot?.querySelector('.block') as HTMLDivElement;
if (!editableElement) {
console.log('getSplitContent: No editable element found');
return null;
}
- // Handle special cases for different block types
- if (this.block.type === 'code') {
- // For code blocks, split text content
- const fullText = editableElement.textContent || '';
- const textNode = this.getFirstTextNode(editableElement);
-
- if (textNode && range.startContainer === textNode) {
- const before = fullText.substring(0, range.startOffset);
- const after = fullText.substring(range.startOffset);
-
- console.log('getSplitContent: Code block split result:', {
- contentLength: fullText.length,
- beforeContent: before,
- beforeLength: before.length,
- afterContent: after,
- afterLength: after.length,
- startOffset: range.startOffset
- });
-
- return { before, after };
- }
- }
+ console.log('getSplitContent: Element info:', {
+ blockType: this.block.type,
+ innerHTML: editableElement.innerHTML,
+ textContent: editableElement.textContent,
+ textLength: editableElement.textContent?.length
+ });
- // For other block types, extract HTML content
- try {
- // If selection is not directly in our element, try to find cursor position by text
- if (!editableElement.contains(range.startContainer)) {
- // Simple approach: split at cursor position in text
- const textContent = editableElement.textContent || '';
- const cursorPos = range.startOffset; // Simplified cursor position
-
- const beforeText = textContent.substring(0, cursorPos);
- const afterText = textContent.substring(cursorPos);
-
- console.log('Splitting by text position (fallback):', {
- cursorPos,
- beforeText,
- afterText,
- totalLength: textContent.length
+ // Get parent wysiwyg component's shadow root
+ const parentComponent = this.closest('dees-input-wysiwyg');
+ const parentShadowRoot = parentComponent?.shadowRoot;
+
+ // Get selection info with both shadow roots for proper traversal
+ const shadowRoots: ShadowRoot[] = [];
+ if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
+ if (this.shadowRoot) shadowRoots.push(this.shadowRoot);
+
+ const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
+ console.log('getSplitContent: Selection info from shadow DOMs:', {
+ selectionInfo,
+ shadowRootsCount: shadowRoots.length
+ });
+
+ if (!selectionInfo) {
+ console.log('getSplitContent: No selection, using last known position:', this.lastKnownCursorPosition);
+ // Try using last known cursor position
+ if (this.lastKnownCursorPosition !== null) {
+ const fullText = editableElement.textContent || '';
+ const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
+ console.log('getSplitContent: Splitting with last known position:', {
+ pos,
+ fullTextLength: fullText.length,
+ before: fullText.substring(0, pos),
+ after: fullText.substring(pos)
});
-
- // For now, return text-based split
return {
- before: beforeText,
- after: afterText
+ before: fullText.substring(0, pos),
+ after: fullText.substring(pos)
};
}
-
- // Create a temporary range to get content before cursor
- const beforeRange = document.createRange();
- beforeRange.selectNodeContents(editableElement);
- beforeRange.setEnd(range.startContainer, range.startOffset);
-
- // Create a temporary range to get content after cursor
- const afterRange = document.createRange();
- afterRange.selectNodeContents(editableElement);
- afterRange.setStart(range.startContainer, range.startOffset);
-
- // Clone HTML content (not extract, to avoid modifying the DOM)
- const beforeContents = beforeRange.cloneContents();
- const afterContents = afterRange.cloneContents();
-
- // Convert to HTML strings
- const tempDiv = document.createElement('div');
- tempDiv.appendChild(beforeContents);
- const beforeHtml = tempDiv.innerHTML;
-
- tempDiv.innerHTML = '';
- tempDiv.appendChild(afterContents);
- const afterHtml = tempDiv.innerHTML;
-
- const result = {
- before: beforeHtml,
- after: afterHtml
- };
-
- console.log('getSplitContent: Split result:', {
- contentLength: fullContent.length,
- beforeContent: result.before,
- beforeLength: result.before.length,
- afterContent: result.after,
- afterLength: result.after.length
- });
-
- return result;
- } catch (error) {
- console.error('Error splitting content:', error);
- // Fallback: return all content as "before"
- const fallbackResult = {
- before: this.getContent(),
- after: ''
- };
-
- console.log('getSplitContent: Fallback result:', {
- beforeContent: fallbackResult.before,
- beforeLength: fallbackResult.before.length,
- afterContent: fallbackResult.after,
- afterLength: fallbackResult.after.length
- });
-
- return fallbackResult;
+ return null;
}
+
+ console.log('getSplitContent: Selection range:', {
+ startContainer: selectionInfo.startContainer,
+ startOffset: selectionInfo.startOffset,
+ startContainerInElement: editableElement.contains(selectionInfo.startContainer)
+ });
+
+ // Make sure the selection is within this block
+ if (!editableElement.contains(selectionInfo.startContainer)) {
+ console.log('getSplitContent: Selection not in this block, using last known position:', this.lastKnownCursorPosition);
+ // Try using last known cursor position
+ if (this.lastKnownCursorPosition !== null) {
+ const fullText = editableElement.textContent || '';
+ const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
+ return {
+ before: fullText.substring(0, pos),
+ after: fullText.substring(pos)
+ };
+ }
+ return null;
+ }
+
+ // For code blocks, use simple text splitting
+ if (this.block.type === 'code') {
+ const cursorPos = this.getCursorPosition(editableElement) || 0;
+ const fullText = editableElement.textContent || '';
+
+ console.log('getSplitContent: Code block split:', {
+ cursorPos,
+ fullTextLength: fullText.length,
+ before: fullText.substring(0, cursorPos),
+ after: fullText.substring(cursorPos)
+ });
+
+ return {
+ before: fullText.substring(0, cursorPos),
+ after: fullText.substring(cursorPos)
+ };
+ }
+
+ // For HTML content, get cursor position first
+ const cursorPos = this.getCursorPosition(editableElement);
+ console.log('getSplitContent: Cursor position for HTML split:', cursorPos);
+
+ if (cursorPos === null || cursorPos === 0) {
+ // If cursor is at start or can't determine position, move all content
+ console.log('getSplitContent: Cursor at start or null, moving all content');
+ return {
+ before: '',
+ after: editableElement.innerHTML
+ };
+ }
+
+ // For HTML content, split using ranges to preserve formatting
+ const beforeRange = document.createRange();
+ const afterRange = document.createRange();
+
+ // Before range: from start of element to cursor
+ beforeRange.setStart(editableElement, 0);
+ beforeRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
+
+ // After range: from cursor to end of element
+ afterRange.setStart(selectionInfo.startContainer, selectionInfo.startOffset);
+ afterRange.setEnd(editableElement, editableElement.childNodes.length);
+
+ // Extract HTML content
+ const beforeFragment = beforeRange.cloneContents();
+ const afterFragment = afterRange.cloneContents();
+
+ // Convert to HTML strings
+ const tempDiv = document.createElement('div');
+ tempDiv.appendChild(beforeFragment);
+ const beforeHtml = tempDiv.innerHTML;
+
+ tempDiv.innerHTML = '';
+ tempDiv.appendChild(afterFragment);
+ const afterHtml = tempDiv.innerHTML;
+
+ console.log('getSplitContent: Final split result:', {
+ cursorPos,
+ beforeHtml,
+ beforeLength: beforeHtml.length,
+ afterHtml,
+ afterLength: afterHtml.length
+ });
+
+ return {
+ before: beforeHtml,
+ after: afterHtml
+ };
}
private handleMouseUp(_e: MouseEvent): void {
@@ -722,7 +783,10 @@ export class DeesWysiwygBlock extends DeesElement {
const range = selection.getRangeAt(0);
// Check if selection is within this block
- if (this.blockElement && this.blockElement.contains(range.commonAncestorContainer)) {
+ 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
@@ -740,117 +804,4 @@ export class DeesWysiwygBlock extends DeesElement {
}
}, 10);
}
-
- /**
- * Logs cursor position for debugging
- */
- private logCursorPosition(eventType: string, event?: KeyboardEvent): void {
- console.log(`[CursorLog] Event triggered: ${eventType} in block ${this.block.id}`);
-
- // Get the actual active element considering shadow DOM
- const activeElement = this.shadowRoot?.activeElement;
- console.log(`[CursorLog] Active element:`, activeElement, 'Block element:', this.blockElement);
-
- // Only log if this block is focused
- if (activeElement !== this.blockElement) {
- console.log(`[CursorLog] Block not focused, skipping detailed logging`);
- return;
- }
-
- // Get selection info using the new utility that handles Shadow DOM
- const selectionInfo = WysiwygSelection.getSelectionInfo(this.shadowRoot!);
- if (!selectionInfo) {
- console.log(`[${eventType}] No selection available`);
- return;
- }
-
- const isInThisBlock = WysiwygSelection.isSelectionInElement(this.blockElement, this.shadowRoot!);
-
- if (!isInThisBlock) {
- return;
- }
-
- // Get cursor position details
- const details: any = {
- event: eventType,
- blockId: this.block.id,
- blockType: this.block.type,
- collapsed: selectionInfo.collapsed,
- startContainer: {
- nodeType: selectionInfo.startContainer.nodeType,
- nodeName: selectionInfo.startContainer.nodeName,
- textContent: selectionInfo.startContainer.textContent?.substring(0, 50) + '...',
- },
- startOffset: selectionInfo.startOffset,
- };
-
- // Add key info if it's a keyboard event
- if (event) {
- details.key = event.key;
- details.shiftKey = event.shiftKey;
- details.ctrlKey = event.ctrlKey;
- details.metaKey = event.metaKey;
- }
-
- // Try to get the actual cursor position in the text
- if (selectionInfo.startContainer.nodeType === Node.TEXT_NODE) {
- const textNode = selectionInfo.startContainer as Text;
- const textBefore = textNode.textContent?.substring(0, selectionInfo.startOffset) || '';
- const textAfter = textNode.textContent?.substring(selectionInfo.startOffset) || '';
-
- details.cursorPosition = {
- textBefore: textBefore.slice(-20), // Last 20 chars before cursor
- textAfter: textAfter.slice(0, 20), // First 20 chars after cursor
- totalLength: textNode.textContent?.length || 0,
- offset: selectionInfo.startOffset
- };
- }
-
- // Check if we're at boundaries
- details.boundaries = {
- atStart: this.isCursorAtStart(selectionInfo),
- atEnd: this.isCursorAtEnd(selectionInfo)
- };
-
- console.log('Cursor Position:', details);
- }
-
- /**
- * Check if cursor is at the start of the block
- */
- private isCursorAtStart(selectionInfo: { startContainer: Node; startOffset: number; collapsed: boolean }): boolean {
- if (!selectionInfo.collapsed || selectionInfo.startOffset !== 0) return false;
-
- const firstNode = this.getFirstTextNode(this.blockElement);
- return !firstNode || selectionInfo.startContainer === firstNode || selectionInfo.startContainer === this.blockElement;
- }
-
- /**
- * Check if cursor is at the end of the block
- */
- private isCursorAtEnd(selectionInfo: { endContainer: Node; endOffset: number; collapsed: boolean }): boolean {
- if (!selectionInfo.collapsed) return false;
-
- const lastNode = this.getLastTextNode(this.blockElement);
- if (!lastNode) return true;
-
- return selectionInfo.endContainer === lastNode &&
- selectionInfo.endOffset === (lastNode.textContent?.length || 0);
- }
-
- /**
- * Get the last text node in the element
- */
- private getLastTextNode(node: Node): Text | null {
- if (node.nodeType === Node.TEXT_NODE) {
- return node as Text;
- }
-
- for (let i = node.childNodes.length - 1; i >= 0; i--) {
- const lastText = this.getLastTextNode(node.childNodes[i]);
- if (lastText) return lastText;
- }
-
- return null;
- }
}
\ No newline at end of file
diff --git a/ts_web/elements/wysiwyg/instructions.md b/ts_web/elements/wysiwyg/instructions.md
index f6240d8..6c7a07e 100644
--- a/ts_web/elements/wysiwyg/instructions.md
+++ b/ts_web/elements/wysiwyg/instructions.md
@@ -2,4 +2,6 @@
* We try to have separated concerns in different classes
* We try to have clean concise and managable code
* lets log whats happening, so if something goes wrong, we understand whats happening.
-* Read https://developer.mozilla.org/en-US/docs/Web/API/Selection/getComposedRanges
\ No newline at end of file
+* Read https://developer.mozilla.org/en-US/docs/Web/API/Selection/getComposedRanges
+* Read https://developer.mozilla.org/en-US/docs/Web/API/Selection/getComposedRanges
+* Make sure to hand over correct shodowroots.
diff --git a/ts_web/elements/wysiwyg/wysiwyg.selection.ts b/ts_web/elements/wysiwyg/wysiwyg.selection.ts
index 41a247c..414ebc7 100644
--- a/ts_web/elements/wysiwyg/wysiwyg.selection.ts
+++ b/ts_web/elements/wysiwyg/wysiwyg.selection.ts
@@ -10,6 +10,9 @@ export interface SelectionInfo {
collapsed: boolean;
}
+// Type for the extended caretPositionFromPoint with Shadow DOM support
+type CaretPositionFromPointExtended = (x: number, y: number, ...shadowRoots: ShadowRoot[]) => CaretPosition | null;
+
export class WysiwygSelection {
/**
* Gets selection info that works across Shadow DOM boundaries
@@ -22,7 +25,8 @@ export class WysiwygSelection {
// Try using getComposedRanges if available (better Shadow DOM support)
if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function') {
try {
- const ranges = selection.getComposedRanges(...shadowRoots);
+ // Pass shadow roots in the correct format as per MDN
+ const ranges = selection.getComposedRanges({ shadowRoots });
if (ranges.length > 0) {
const range = ranges[0];
return {
@@ -139,6 +143,66 @@ export class WysiwygSelection {
}
}
+ /**
+ * Gets cursor position from mouse coordinates with Shadow DOM support
+ */
+ static getCursorPositionFromPoint(x: number, y: number, container: HTMLElement, ...shadowRoots: ShadowRoot[]): number | null {
+ // Try modern API with shadow root support
+ if ('caretPositionFromPoint' in document && document.caretPositionFromPoint) {
+ let caretPos: CaretPosition | null = null;
+
+ // Try with shadow roots first (newer API)
+ try {
+ caretPos = (document.caretPositionFromPoint as any)(x, y, ...shadowRoots);
+ } catch (e) {
+ // Fallback to standard API without shadow roots
+ caretPos = document.caretPositionFromPoint(x, y);
+ }
+
+ if (caretPos && container.contains(caretPos.offsetNode)) {
+ // Calculate total offset within the container
+ return this.getOffsetInElement(caretPos.offsetNode, caretPos.offset, container);
+ }
+ }
+
+ // Safari/WebKit fallback
+ if ('caretRangeFromPoint' in document) {
+ const range = (document as any).caretRangeFromPoint(x, y);
+ if (range && container.contains(range.startContainer)) {
+ return this.getOffsetInElement(range.startContainer, range.startOffset, container);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Helper to get the total character offset of a position within an element
+ */
+ private static getOffsetInElement(node: Node, offset: number, container: HTMLElement): number {
+ let totalOffset = 0;
+ let found = false;
+
+ const walker = document.createTreeWalker(
+ container,
+ NodeFilter.SHOW_TEXT,
+ null
+ );
+
+ let textNode: Node | null;
+ while (textNode = walker.nextNode()) {
+ if (textNode === node) {
+ totalOffset += offset;
+ found = true;
+ break;
+ } else {
+ totalOffset += textNode.textContent?.length || 0;
+ }
+ }
+
+ return found ? totalOffset : 0;
+ }
+
/**
* Sets cursor position in an element
*/