diff --git a/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts b/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts index 4fa3aea02bc..8b1168cd7c7 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts @@ -153,18 +153,15 @@ function setRangeSelection(core: EditorCore, element: HTMLElement | undefined, c if (element && core.domHelper.isNodeInEditor(element)) { const doc = core.physicalRoot.ownerDocument; const range = doc.createRange(); - let isReverted: boolean | undefined = undefined; + let isReverted = false; range.selectNode(element); if (collapse) { range.collapse(); } else { - const selection = doc.defaultView?.getSelection(); - const range = selection && selection.rangeCount > 0 && selection.getRangeAt(0); - if (selection && range) { - isReverted = - selection.focusNode != range.endContainer || - selection.focusOffset != range.endOffset; + const currentRange = core.domHelper.getSelectionRange(); + if (currentRange) { + isReverted = core.domHelper.isSelectionReverted(currentRange); } } diff --git a/packages/roosterjs-content-model-core/lib/editor/Editor.ts b/packages/roosterjs-content-model-core/lib/editor/Editor.ts index a0fe1140632..3ff23dd9878 100644 --- a/packages/roosterjs-content-model-core/lib/editor/Editor.ts +++ b/packages/roosterjs-content-model-core/lib/editor/Editor.ts @@ -80,6 +80,7 @@ export class Editor implements IEditor { } core.darkColorHandler.reset(); + core.domHelper?.dispose?.(); this.core = null; } diff --git a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts index fcf9abf0ba8..3cb6b4788a2 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts @@ -44,6 +44,10 @@ class DOMHelperImpl implements DOMHelper { private shadowRoot: ShadowRoot | null; private doc: Document; private useComposedRanges: boolean; + private useBeforeInputPolyfill: boolean; + private polyfillRange: Range | null = null; + private polyfillProcessing: boolean = false; + private polyfillDisposer: (() => void) | null = null; constructor(private contentDiv: HTMLElement, options?: DOMHelperImplOption) { const rootNode = contentDiv.getRootNode(); @@ -52,6 +56,83 @@ class DOMHelperImpl implements DOMHelper { const sel = this.doc.defaultView?.getSelection(); this.useComposedRanges = !!(this.shadowRoot && sel && 'getComposedRanges' in sel); + + // For old Safari that has no getComposedRanges but supports beforeinput/getTargetRanges + const supportsBeforeInput = !!( + this.doc.defaultView && + 'InputEvent' in this.doc.defaultView && + 'getTargetRanges' in (this.doc.defaultView as any).InputEvent.prototype + ); + this.useBeforeInputPolyfill = !!( + this.shadowRoot && + !this.useComposedRanges && + supportsBeforeInput + ); + + if (this.useBeforeInputPolyfill) { + this.initBeforeInputPolyfill(); + } + } + + private initBeforeInputPolyfill(): void { + const win = this.doc.defaultView!; + + const onSelectionChange = () => { + if (!this.polyfillProcessing) { + this.polyfillProcessing = true; + + const active = this.getActiveElementInShadow(); + if ( + active && + active.getAttribute('contenteditable') === 'true' && + this.contentDiv.contains(active) + ) { + this.doc.execCommand('indent'); + } else { + this.polyfillRange = null; + } + + this.polyfillProcessing = false; + } + }; + + const onBeforeInput = (event: InputEvent) => { + if (this.polyfillProcessing) { + const ranges = event.getTargetRanges(); + if (ranges.length > 0) { + const sr = ranges[0]; + const range = this.doc.createRange(); + range.setStart(sr.startContainer, sr.startOffset); + range.setEnd(sr.endContainer, sr.endOffset); + this.polyfillRange = range; + } + + event.preventDefault(); + event.stopImmediatePropagation(); + } + }; + + const onSelectStart = () => { + this.polyfillRange = null; + }; + + win.addEventListener('selectionchange', onSelectionChange, true); + win.addEventListener('beforeinput', onBeforeInput as EventListener, true); + win.addEventListener('selectstart', onSelectStart, true); + + this.polyfillDisposer = () => { + win.removeEventListener('selectionchange', onSelectionChange, true); + win.removeEventListener('beforeinput', onBeforeInput as EventListener, true); + win.removeEventListener('selectstart', onSelectStart, true); + }; + } + + private getActiveElementInShadow(): Element | null { + let active: Element | null = this.doc.activeElement; + while (active && active.shadowRoot && active.shadowRoot.activeElement) { + active = active.shadowRoot.activeElement; + } + return active; } queryElements(selector: string): HTMLElement[] { @@ -218,17 +299,30 @@ class DOMHelperImpl implements DOMHelper { } getSelectionRange(): Range | null { + if (this.useBeforeInputPolyfill) { + return this.polyfillRange; + } + const sel = this.doc.defaultView?.getSelection(); if (!sel) { return null; } if (this.useComposedRanges && this.shadowRoot && isSelectionWithComposedRanges(sel)) { - const staticRanges = sel.getComposedRanges({ - shadowRoots: [this.shadowRoot], - }); + // Safari 17.4+ uses options dict; Safari 17.2-17.3 uses rest params + let staticRanges: StaticRange[] = []; + try { + staticRanges = (sel as any).getComposedRanges({ + shadowRoots: [this.shadowRoot], + }); + } catch { + try { + // Fallback for Safari 17.2-17.3 which uses rest parameter syntax + staticRanges = (sel as any).getComposedRanges(this.shadowRoot); + } catch {} + } - if (staticRanges?.length > 0) { + if (staticRanges.length > 0) { const sr = staticRanges[0]; const range = this.doc.createRange(); range.setStart(sr.startContainer, sr.startOffset); @@ -263,6 +357,14 @@ class DOMHelperImpl implements DOMHelper { this.doc.body.appendChild(element); } } + + dispose(): void { + if (this.polyfillDisposer) { + this.polyfillDisposer(); + this.polyfillDisposer = null; + } + this.polyfillRange = null; + } } /**