update
This commit is contained in:
@ -6,7 +6,6 @@ import {
|
||||
type TemplateResult,
|
||||
cssManager,
|
||||
css,
|
||||
query,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { type IBlock } from './wysiwyg.types.js';
|
||||
@ -38,12 +37,15 @@ 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,
|
||||
css`
|
||||
@ -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`<div class="wysiwyg-block-container"></div>`;
|
||||
}
|
||||
|
||||
private renderBlockContent(): string {
|
||||
if (!this.block) return '';
|
||||
|
||||
if (this.block.type === 'divider') {
|
||||
return html`
|
||||
return `
|
||||
<div class="block divider" data-block-id="${this.block.id}" data-block-type="${this.block.type}">
|
||||
<hr>
|
||||
</div>
|
||||
@ -349,11 +395,12 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
|
||||
if (this.block.type === 'code') {
|
||||
const language = this.block.metadata?.language || 'plain text';
|
||||
return html`
|
||||
const selectedClass = this.isSelected ? ' selected' : '';
|
||||
return `
|
||||
<div class="code-block-container">
|
||||
<div class="code-language">${language}</div>
|
||||
<div
|
||||
class="block code ${this.isSelected ? 'selected' : ''}"
|
||||
class="block code${selectedClass}"
|
||||
contenteditable="true"
|
||||
data-block-type="${this.block.type}"
|
||||
></div>
|
||||
@ -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 `
|
||||
<div
|
||||
class="block ${this.block.type} ${this.isSelected ? 'selected' : ''}"
|
||||
class="block ${this.block.type}${selectedClass}"
|
||||
contenteditable="true"
|
||||
data-placeholder="${placeholder}"
|
||||
></div>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
for (let i = 0; i < node.childNodes.length; i++) {
|
||||
const textNode = this.getFirstTextNode(node.childNodes[i]);
|
||||
if (textNode) return textNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
console.log('getSplitContent: Element info:', {
|
||||
blockType: this.block.type,
|
||||
innerHTML: editableElement.innerHTML,
|
||||
textContent: editableElement.textContent,
|
||||
textLength: editableElement.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 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
|
||||
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)
|
||||
});
|
||||
|
||||
return { before, after };
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
// For now, return text-based split
|
||||
return {
|
||||
before: beforeText,
|
||||
after: afterText
|
||||
before: fullText.substring(0, pos),
|
||||
after: fullText.substring(pos)
|
||||
};
|
||||
}
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
// Create a temporary range to get content before cursor
|
||||
// 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();
|
||||
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();
|
||||
// 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(beforeContents);
|
||||
tempDiv.appendChild(beforeFragment);
|
||||
const beforeHtml = tempDiv.innerHTML;
|
||||
|
||||
tempDiv.innerHTML = '';
|
||||
tempDiv.appendChild(afterContents);
|
||||
tempDiv.appendChild(afterFragment);
|
||||
const afterHtml = tempDiv.innerHTML;
|
||||
|
||||
const result = {
|
||||
console.log('getSplitContent: Final split result:', {
|
||||
cursorPos,
|
||||
beforeHtml,
|
||||
beforeLength: beforeHtml.length,
|
||||
afterHtml,
|
||||
afterLength: afterHtml.length
|
||||
});
|
||||
|
||||
return {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -3,3 +3,5 @@
|
||||
* 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
|
||||
* Read https://developer.mozilla.org/en-US/docs/Web/API/Selection/getComposedRanges
|
||||
* Make sure to hand over correct shodowroots.
|
||||
|
@ -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
|
||||
*/
|
||||
|
Reference in New Issue
Block a user