This commit is contained in:
Juergen Kunz
2025-06-24 16:49:40 +00:00
parent 366544befc
commit 1041814823
4 changed files with 200 additions and 148 deletions

View File

@ -20,6 +20,13 @@ declare global {
@customElement('dees-wysiwyg-block')
export class DeesWysiwygBlock extends DeesElement {
async disconnectedCallback() {
await super.disconnectedCallback();
// Clean up selection handler
if ((this as any)._selectionHandler) {
document.removeEventListener('selectionchange', (this as any)._selectionHandler);
}
}
@property({ type: Object })
public block: IBlock;
@ -45,6 +52,7 @@ export class DeesWysiwygBlock extends DeesElement {
// Track cursor position
private lastKnownCursorPosition: number = 0;
private lastSelectedText: string = '';
public static styles = [
cssManager.defaultStyles,
@ -309,20 +317,6 @@ export class DeesWysiwygBlock extends DeesElement {
this.handlers?.onKeyDown?.(e);
});
editableBlock.addEventListener('keyup', (e) => {
// Track cursor position after key release
const pos = this.getCursorPosition(editableBlock);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
}
// Check for selection after keyboard navigation
if (e.shiftKey || ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
setTimeout(() => {
this.checkForTextSelection();
}, 10);
}
});
editableBlock.addEventListener('focus', () => {
this.handlers?.onFocus?.();
@ -341,21 +335,13 @@ export class DeesWysiwygBlock extends DeesElement {
});
editableBlock.addEventListener('mouseup', (e) => {
// 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);
}
// Check for text selection with a longer delay
setTimeout(() => {
this.checkForTextSelection();
}, 50);
}, 0);
const pos = this.getCursorPosition(editableBlock);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
console.log('Cursor position after mouseup:', pos);
}
this.handleMouseUp(e);
// Selection will be handled by selectionchange event
this.handlers?.onMouseUp?.(e);
});
@ -370,28 +356,102 @@ export class DeesWysiwygBlock extends DeesElement {
}, 0);
});
// Add select event listener
editableBlock.addEventListener('selectstart', () => {
console.log('Selection started in block');
});
// Listen for selection changes with a mutation observer
let selectionCheckTimeout: any = null;
const checkSelectionDebounced = () => {
if (selectionCheckTimeout) clearTimeout(selectionCheckTimeout);
selectionCheckTimeout = setTimeout(() => {
this.checkForTextSelection();
}, 100);
// Add selection change handler
const checkSelection = () => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return;
const selectedText = selection.toString();
if (selectedText.length === 0) {
// Clear selection if no text
if (this.lastSelectedText) {
this.lastSelectedText = '';
this.dispatchEvent(new CustomEvent('block-text-selected', {
detail: {
text: '',
blockId: this.block.id,
hasSelection: false
},
bubbles: true,
composed: true
}));
}
return;
}
// Get fresh reference to the editable block
const currentEditableBlock = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.shadowRoot?.querySelector('.block') as HTMLDivElement;
if (!currentEditableBlock) return;
// Get parent wysiwyg component's shadow root
const parentComponent = this.closest('dees-input-wysiwyg');
const parentShadowRoot = parentComponent?.shadowRoot;
// Use getComposedRanges with shadow roots as per MDN docs
const shadowRoots: ShadowRoot[] = [];
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
if (this.shadowRoot) shadowRoots.push(this.shadowRoot);
// Get selection info using our Shadow DOM-aware utility
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
if (!selectionInfo) return;
// Check if selection is within this block
const startInBlock = currentEditableBlock.contains(selectionInfo.startContainer);
const endInBlock = currentEditableBlock.contains(selectionInfo.endContainer);
if (startInBlock || endInBlock) {
if (selectedText !== this.lastSelectedText) {
this.lastSelectedText = selectedText;
console.log('✅ Selection detected in block using getComposedRanges:', selectedText);
// Create range and get rect
const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
const rect = range.getBoundingClientRect();
// Dispatch event
this.dispatchEvent(new CustomEvent('block-text-selected', {
detail: {
text: selectedText.trim(),
blockId: this.block.id,
range: range,
rect: rect,
hasSelection: true
},
bubbles: true,
composed: true
}));
}
} else if (this.lastSelectedText) {
// Clear selection if no longer in this block
this.lastSelectedText = '';
this.dispatchEvent(new CustomEvent('block-text-selected', {
detail: {
text: '',
blockId: this.block.id,
hasSelection: false
},
bubbles: true,
composed: true
}));
}
};
// Check selection on various events
editableBlock.addEventListener('mouseup', checkSelectionDebounced);
editableBlock.addEventListener('keyup', checkSelectionDebounced);
document.addEventListener('selectionchange', () => {
// Check if this block has focus
if (document.activeElement === editableBlock ||
this.shadowRoot?.activeElement === editableBlock) {
checkSelectionDebounced();
// Listen for selection changes
document.addEventListener('selectionchange', checkSelection);
// Store the handler for cleanup
(this as any)._selectionHandler = checkSelection;
// Add keyup handler for cursor position tracking
editableBlock.addEventListener('keyup', (e) => {
// Track cursor position
const pos = this.getCursorPosition(editableBlock);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
}
});
@ -814,72 +874,4 @@ export class DeesWysiwygBlock extends DeesElement {
};
}
private handleMouseUp(_e: MouseEvent): void {
// Selection check is now handled in the mouseup event listener
}
/**
* Check if there's text selected within this block
*/
private checkForTextSelection(): void {
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.shadowRoot?.querySelector('.block') as HTMLDivElement;
if (!editableElement) {
console.log('checkForTextSelection: No editable element found');
return;
}
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
console.log('checkForTextSelection: No selection or range count is 0');
return;
}
const selectedText = selection.toString();
console.log('checkForTextSelection: Selected text raw:', selectedText, 'length:', selectedText.length);
// Only proceed if we have selected text
if (selectedText.trim().length === 0) {
console.log('checkForTextSelection: Selected text is empty after trim');
return;
}
// Check if the selection is within this block
const range = selection.getRangeAt(0);
console.log('checkForTextSelection: Range:', {
startContainer: range.startContainer,
endContainer: range.endContainer,
collapsed: range.collapsed
});
// Check if both start and end are within our editable element
const startInBlock = editableElement.contains(range.startContainer);
const endInBlock = editableElement.contains(range.endContainer);
console.log('checkForTextSelection: Start in block:', startInBlock, 'End in block:', endInBlock);
if (startInBlock && endInBlock) {
console.log('✅ Block detected text selection:', selectedText.trim());
// Get the bounding rect of the selection
const rect = range.getBoundingClientRect();
console.log('Selection rect:', rect);
// Dispatch event to parent with selection details
this.dispatchEvent(new CustomEvent('block-text-selected', {
detail: {
text: selectedText.trim(),
blockId: this.block.id,
range: range,
rect: rect,
hasSelection: true
},
bubbles: true,
composed: true
}));
} else {
console.log('checkForTextSelection: Selection not contained in block');
}
}
}