fix(wysiwyg): cursor position
This commit is contained in:
@ -332,10 +332,8 @@ export class DeesWysiwygBlock extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update blockElement reference for code blocks
|
// For code blocks, we use the nested editableBlock
|
||||||
if (this.block.type === 'code') {
|
// The blockElement getter will automatically find the right element
|
||||||
this.blockElement = editableBlock;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
@ -394,33 +392,43 @@ export class DeesWysiwygBlock extends DeesElement {
|
|||||||
|
|
||||||
|
|
||||||
public focus(): void {
|
public focus(): void {
|
||||||
if (!this.blockElement) return;
|
// 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;
|
||||||
|
|
||||||
|
if (!editableElement) return;
|
||||||
|
|
||||||
// Ensure the element is focusable
|
// Ensure the element is focusable
|
||||||
if (!this.blockElement.hasAttribute('contenteditable')) {
|
if (!editableElement.hasAttribute('contenteditable')) {
|
||||||
this.blockElement.setAttribute('contenteditable', 'true');
|
editableElement.setAttribute('contenteditable', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.blockElement.focus();
|
editableElement.focus();
|
||||||
|
|
||||||
// If focus failed, try again after a microtask
|
// If focus failed, try again after a microtask
|
||||||
if (document.activeElement !== this.blockElement) {
|
if (document.activeElement !== editableElement && this.shadowRoot?.activeElement !== editableElement) {
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
this.blockElement.focus();
|
editableElement.focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public focusWithCursor(position: 'start' | 'end' | number = 'end'): void {
|
public focusWithCursor(position: 'start' | 'end' | number = 'end'): void {
|
||||||
if (!this.blockElement) return;
|
// 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;
|
||||||
|
|
||||||
|
if (!editableElement) return;
|
||||||
|
|
||||||
// Ensure element is focusable first
|
// Ensure element is focusable first
|
||||||
if (!this.blockElement.hasAttribute('contenteditable')) {
|
if (!editableElement.hasAttribute('contenteditable')) {
|
||||||
this.blockElement.setAttribute('contenteditable', 'true');
|
editableElement.setAttribute('contenteditable', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus the element
|
// Focus the element
|
||||||
this.blockElement.focus();
|
editableElement.focus();
|
||||||
|
|
||||||
// Set cursor position after focus is established
|
// Set cursor position after focus is established
|
||||||
const setCursor = () => {
|
const setCursor = () => {
|
||||||
@ -430,17 +438,17 @@ export class DeesWysiwygBlock extends DeesElement {
|
|||||||
this.setCursorToEnd();
|
this.setCursorToEnd();
|
||||||
} else if (typeof position === 'number') {
|
} else if (typeof position === 'number') {
|
||||||
// Use the new selection utility to set cursor position
|
// Use the new selection utility to set cursor position
|
||||||
WysiwygSelection.setCursorPosition(this.blockElement, position);
|
WysiwygSelection.setCursorPosition(editableElement, position);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure cursor is set after focus
|
// Ensure cursor is set after focus
|
||||||
if (document.activeElement === this.blockElement) {
|
if (document.activeElement === editableElement || this.shadowRoot?.activeElement === editableElement) {
|
||||||
setCursor();
|
setCursor();
|
||||||
} else {
|
} else {
|
||||||
// Wait for focus to be established
|
// Wait for focus to be established
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
if (document.activeElement === this.blockElement) {
|
if (document.activeElement === editableElement || this.shadowRoot?.activeElement === editableElement) {
|
||||||
setCursor();
|
setCursor();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -461,44 +469,64 @@ export class DeesWysiwygBlock extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getContent(): string {
|
public getContent(): string {
|
||||||
if (!this.blockElement) return '';
|
// 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;
|
||||||
|
|
||||||
|
if (!editableElement) return '';
|
||||||
|
|
||||||
if (this.block.type === 'list') {
|
if (this.block.type === 'list') {
|
||||||
const listItems = this.blockElement.querySelectorAll('li');
|
const listItems = editableElement.querySelectorAll('li');
|
||||||
return Array.from(listItems).map(li => li.innerHTML || '').join('\n');
|
return Array.from(listItems).map(li => li.innerHTML || '').join('\n');
|
||||||
} else if (this.block.type === 'code') {
|
} else if (this.block.type === 'code') {
|
||||||
return this.blockElement.textContent || '';
|
return editableElement.textContent || '';
|
||||||
} else {
|
} else {
|
||||||
return this.blockElement.innerHTML || '';
|
return editableElement.innerHTML || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public setContent(content: string): void {
|
public setContent(content: string): void {
|
||||||
if (!this.blockElement) return;
|
// 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;
|
||||||
|
|
||||||
|
if (!editableElement) return;
|
||||||
|
|
||||||
// Store if we have focus
|
// Store if we have focus
|
||||||
const hadFocus = document.activeElement === this.blockElement;
|
const hadFocus = document.activeElement === editableElement || this.shadowRoot?.activeElement === editableElement;
|
||||||
|
|
||||||
if (this.block.type === 'list') {
|
if (this.block.type === 'list') {
|
||||||
this.blockElement.innerHTML = WysiwygBlocks.renderListContent(content, this.block.metadata);
|
editableElement.innerHTML = WysiwygBlocks.renderListContent(content, this.block.metadata);
|
||||||
} else if (this.block.type === 'code') {
|
} else if (this.block.type === 'code') {
|
||||||
this.blockElement.textContent = content;
|
editableElement.textContent = content;
|
||||||
} else {
|
} else {
|
||||||
this.blockElement.innerHTML = content;
|
editableElement.innerHTML = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore focus if we had it
|
// Restore focus if we had it
|
||||||
if (hadFocus) {
|
if (hadFocus) {
|
||||||
this.blockElement.focus();
|
editableElement.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public setCursorToStart(): void {
|
public setCursorToStart(): void {
|
||||||
WysiwygBlocks.setCursorToStart(this.blockElement);
|
const editableElement = this.block?.type === 'code'
|
||||||
|
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
|
||||||
|
: this.blockElement;
|
||||||
|
if (editableElement) {
|
||||||
|
WysiwygBlocks.setCursorToStart(editableElement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public setCursorToEnd(): void {
|
public setCursorToEnd(): void {
|
||||||
WysiwygBlocks.setCursorToEnd(this.blockElement);
|
const editableElement = this.block?.type === 'code'
|
||||||
|
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
|
||||||
|
: this.blockElement;
|
||||||
|
if (editableElement) {
|
||||||
|
WysiwygBlocks.setCursorToEnd(editableElement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public focusListItem(): void {
|
public focusListItem(): void {
|
||||||
@ -521,50 +549,84 @@ export class DeesWysiwygBlock extends DeesElement {
|
|||||||
blockType: this.block.type
|
blockType: this.block.type
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get selection info using the new utility that handles Shadow DOM
|
// Direct approach: Get selection from window
|
||||||
const selectionInfo = WysiwygSelection.getSelectionInfo(this.shadowRoot!);
|
const selection = window.getSelection();
|
||||||
if (!selectionInfo) {
|
if (!selection || selection.rangeCount === 0) {
|
||||||
console.log('getSplitContent: No selection, returning all content as before');
|
console.log('getSplitContent: No selection found');
|
||||||
return {
|
return {
|
||||||
before: fullContent,
|
before: fullContent,
|
||||||
after: ''
|
after: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if selection is within this block
|
const range = selection.getRangeAt(0);
|
||||||
if (!WysiwygSelection.isSelectionInElement(this.blockElement, this.shadowRoot!)) {
|
console.log('getSplitContent: Range info:', {
|
||||||
console.log('getSplitContent: Selection not in this block');
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get cursor position as a number
|
// Get the actual editable element (might be nested for code blocks)
|
||||||
const cursorPosition = WysiwygSelection.getCursorPositionInElement(this.blockElement, this.shadowRoot!);
|
const editableElement = this.block.type === 'code'
|
||||||
console.log('getSplitContent: Cursor position:', {
|
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
|
||||||
cursorPosition,
|
: this.blockElement;
|
||||||
contentLength: fullContent.length,
|
|
||||||
startContainer: selectionInfo.startContainer,
|
if (!editableElement) {
|
||||||
startOffset: selectionInfo.startOffset,
|
console.log('getSplitContent: No editable element found');
|
||||||
collapsed: selectionInfo.collapsed
|
return null;
|
||||||
});
|
}
|
||||||
|
|
||||||
// Handle special cases for different block types
|
// Handle special cases for different block types
|
||||||
if (this.block.type === 'code') {
|
if (this.block.type === 'code') {
|
||||||
// For code blocks, split text content
|
// For code blocks, split text content
|
||||||
const fullText = this.blockElement.textContent || '';
|
const fullText = editableElement.textContent || '';
|
||||||
const textNode = this.getFirstTextNode(this.blockElement);
|
const textNode = this.getFirstTextNode(editableElement);
|
||||||
|
|
||||||
if (textNode && selectionInfo.startContainer === textNode) {
|
if (textNode && range.startContainer === textNode) {
|
||||||
const before = fullText.substring(0, selectionInfo.startOffset);
|
const before = fullText.substring(0, range.startOffset);
|
||||||
const after = fullText.substring(selectionInfo.startOffset);
|
const after = fullText.substring(range.startOffset);
|
||||||
|
|
||||||
console.log('getSplitContent: Code block split result:', {
|
console.log('getSplitContent: Code block split result:', {
|
||||||
cursorPosition,
|
|
||||||
contentLength: fullText.length,
|
contentLength: fullText.length,
|
||||||
beforeContent: before,
|
beforeContent: before,
|
||||||
beforeLength: before.length,
|
beforeLength: before.length,
|
||||||
afterContent: after,
|
afterContent: after,
|
||||||
afterLength: after.length,
|
afterLength: after.length,
|
||||||
startOffset: selectionInfo.startOffset
|
startOffset: range.startOffset
|
||||||
});
|
});
|
||||||
|
|
||||||
return { before, after };
|
return { before, after };
|
||||||
@ -573,15 +635,38 @@ export class DeesWysiwygBlock extends DeesElement {
|
|||||||
|
|
||||||
// For other block types, extract HTML content
|
// For other block types, extract HTML content
|
||||||
try {
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Create a temporary range to get content before cursor
|
// Create a temporary range to get content before cursor
|
||||||
const beforeRange = document.createRange();
|
const beforeRange = document.createRange();
|
||||||
beforeRange.selectNodeContents(this.blockElement);
|
beforeRange.selectNodeContents(editableElement);
|
||||||
beforeRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
beforeRange.setEnd(range.startContainer, range.startOffset);
|
||||||
|
|
||||||
// Create a temporary range to get content after cursor
|
// Create a temporary range to get content after cursor
|
||||||
const afterRange = document.createRange();
|
const afterRange = document.createRange();
|
||||||
afterRange.selectNodeContents(this.blockElement);
|
afterRange.selectNodeContents(editableElement);
|
||||||
afterRange.setStart(selectionInfo.startContainer, selectionInfo.startOffset);
|
afterRange.setStart(range.startContainer, range.startOffset);
|
||||||
|
|
||||||
// Clone HTML content (not extract, to avoid modifying the DOM)
|
// Clone HTML content (not extract, to avoid modifying the DOM)
|
||||||
const beforeContents = beforeRange.cloneContents();
|
const beforeContents = beforeRange.cloneContents();
|
||||||
@ -602,7 +687,6 @@ export class DeesWysiwygBlock extends DeesElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
console.log('getSplitContent: Split result:', {
|
console.log('getSplitContent: Split result:', {
|
||||||
cursorPosition,
|
|
||||||
contentLength: fullContent.length,
|
contentLength: fullContent.length,
|
||||||
beforeContent: result.before,
|
beforeContent: result.before,
|
||||||
beforeLength: result.before.length,
|
beforeLength: result.before.length,
|
||||||
|
@ -100,9 +100,9 @@ export class WysiwygSelection {
|
|||||||
/**
|
/**
|
||||||
* Gets cursor position relative to a specific element
|
* Gets cursor position relative to a specific element
|
||||||
*/
|
*/
|
||||||
static getCursorPositionInElement(element: Element, shadowRoot?: ShadowRoot): number | null {
|
static getCursorPositionInElement(element: Element, ...shadowRoots: ShadowRoot[]): number | null {
|
||||||
const selectionInfo = shadowRoot
|
const selectionInfo = shadowRoots.length > 0
|
||||||
? this.getSelectionInfo(shadowRoot)
|
? this.getSelectionInfo(...shadowRoots)
|
||||||
: this.getSelectionInfo();
|
: this.getSelectionInfo();
|
||||||
|
|
||||||
if (!selectionInfo || !selectionInfo.collapsed) return null;
|
if (!selectionInfo || !selectionInfo.collapsed) return null;
|
||||||
@ -111,9 +111,28 @@ export class WysiwygSelection {
|
|||||||
try {
|
try {
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.selectNodeContents(element);
|
range.selectNodeContents(element);
|
||||||
range.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
|
||||||
|
|
||||||
|
// Handle case where selection is in a text node that's a child of the element
|
||||||
|
if (element.contains(selectionInfo.startContainer)) {
|
||||||
|
range.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
||||||
return range.toString().length;
|
return range.toString().length;
|
||||||
|
} else {
|
||||||
|
// Selection might be in shadow DOM or different context
|
||||||
|
// Try to find the equivalent position in the element
|
||||||
|
const text = element.textContent || '';
|
||||||
|
const selectionText = selectionInfo.startContainer.textContent || '';
|
||||||
|
|
||||||
|
// If the selection is at the beginning or end, handle those cases
|
||||||
|
if (selectionInfo.startOffset === 0) {
|
||||||
|
return 0;
|
||||||
|
} else if (selectionInfo.startOffset === selectionText.length) {
|
||||||
|
return text.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other cases, try to match based on text content
|
||||||
|
console.warn('Selection container not within element, using text matching fallback');
|
||||||
|
return selectionInfo.startOffset;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to get cursor position:', error);
|
console.warn('Failed to get cursor position:', error);
|
||||||
return null;
|
return null;
|
||||||
|
Reference in New Issue
Block a user