157 lines
4.4 KiB
TypeScript
157 lines
4.4 KiB
TypeScript
/**
|
|
* Utilities for handling selection across Shadow DOM boundaries
|
|
*/
|
|
|
|
export interface SelectionInfo {
|
|
startContainer: Node;
|
|
startOffset: number;
|
|
endContainer: Node;
|
|
endOffset: number;
|
|
collapsed: boolean;
|
|
}
|
|
|
|
export class WysiwygSelection {
|
|
/**
|
|
* Gets selection info that works across Shadow DOM boundaries
|
|
* @param shadowRoots - Shadow roots to include in the selection search
|
|
*/
|
|
static getSelectionInfo(...shadowRoots: ShadowRoot[]): SelectionInfo | null {
|
|
const selection = window.getSelection();
|
|
if (!selection) return null;
|
|
|
|
// Try using getComposedRanges if available (better Shadow DOM support)
|
|
if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function') {
|
|
try {
|
|
const ranges = selection.getComposedRanges(...shadowRoots);
|
|
if (ranges.length > 0) {
|
|
const range = ranges[0];
|
|
return {
|
|
startContainer: range.startContainer,
|
|
startOffset: range.startOffset,
|
|
endContainer: range.endContainer,
|
|
endOffset: range.endOffset,
|
|
collapsed: range.collapsed
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.warn('getComposedRanges failed, falling back to getRangeAt:', error);
|
|
}
|
|
}
|
|
|
|
// Fallback to traditional selection API
|
|
if (selection.rangeCount > 0) {
|
|
const range = selection.getRangeAt(0);
|
|
return {
|
|
startContainer: range.startContainer,
|
|
startOffset: range.startOffset,
|
|
endContainer: range.endContainer,
|
|
endOffset: range.endOffset,
|
|
collapsed: range.collapsed
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Checks if a selection is within a specific element (considering Shadow DOM)
|
|
*/
|
|
static isSelectionInElement(element: Element, shadowRoot?: ShadowRoot): boolean {
|
|
const selectionInfo = shadowRoot
|
|
? this.getSelectionInfo(shadowRoot)
|
|
: this.getSelectionInfo();
|
|
|
|
if (!selectionInfo) return false;
|
|
|
|
// Check if the selection's common ancestor is within the element
|
|
return element.contains(selectionInfo.startContainer) ||
|
|
element.contains(selectionInfo.endContainer);
|
|
}
|
|
|
|
/**
|
|
* Gets the selected text across Shadow DOM boundaries
|
|
*/
|
|
static getSelectedText(): string {
|
|
const selection = window.getSelection();
|
|
return selection ? selection.toString() : '';
|
|
}
|
|
|
|
/**
|
|
* Creates a range from selection info
|
|
*/
|
|
static createRangeFromInfo(info: SelectionInfo): Range {
|
|
const range = document.createRange();
|
|
range.setStart(info.startContainer, info.startOffset);
|
|
range.setEnd(info.endContainer, info.endOffset);
|
|
return range;
|
|
}
|
|
|
|
/**
|
|
* Sets selection from a range (works with Shadow DOM)
|
|
*/
|
|
static setSelectionFromRange(range: Range): void {
|
|
const selection = window.getSelection();
|
|
if (selection) {
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets cursor position relative to a specific element
|
|
*/
|
|
static getCursorPositionInElement(element: Element, shadowRoot?: ShadowRoot): number | null {
|
|
const selectionInfo = shadowRoot
|
|
? this.getSelectionInfo(shadowRoot)
|
|
: this.getSelectionInfo();
|
|
|
|
if (!selectionInfo || !selectionInfo.collapsed) return null;
|
|
|
|
// Create a range from start of element to cursor position
|
|
try {
|
|
const range = document.createRange();
|
|
range.selectNodeContents(element);
|
|
range.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
|
|
|
return range.toString().length;
|
|
} catch (error) {
|
|
console.warn('Failed to get cursor position:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets cursor position in an element
|
|
*/
|
|
static setCursorPosition(element: Element, position: number): void {
|
|
const walker = document.createTreeWalker(
|
|
element,
|
|
NodeFilter.SHOW_TEXT,
|
|
null
|
|
);
|
|
|
|
let currentPosition = 0;
|
|
let targetNode: Text | null = null;
|
|
let targetOffset = 0;
|
|
|
|
while (walker.nextNode()) {
|
|
const node = walker.currentNode as Text;
|
|
const nodeLength = node.textContent?.length || 0;
|
|
|
|
if (currentPosition + nodeLength >= position) {
|
|
targetNode = node;
|
|
targetOffset = position - currentPosition;
|
|
break;
|
|
}
|
|
|
|
currentPosition += nodeLength;
|
|
}
|
|
|
|
if (targetNode) {
|
|
const range = document.createRange();
|
|
range.setStart(targetNode, targetOffset);
|
|
range.collapse(true);
|
|
this.setSelectionFromRange(range);
|
|
}
|
|
}
|
|
} |