From 536031c0ce81acf56516c23466d2e92f78b84317 Mon Sep 17 00:00:00 2001 From: Roman Kulakov Date: Wed, 21 Sep 2022 16:14:40 +0300 Subject: [PATCH] Use custom getSelection for Safari Shadow DOM, as native method returns incorrect anchorNode and focusNode --- src/dom.ts | 6 ++++++ src/index.ts | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/dom.ts b/src/dom.ts index 7d7c6306..19a48f31 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -94,3 +94,9 @@ export function keyEvent(keyCode: number, key: string) { ;(event as any).key = (event as any).code = key return event } + +export function deepActiveElement(doc: Document) { + let elt = doc.activeElement + while (elt && elt.shadowRoot) elt = elt.shadowRoot.activeElement + return elt +} diff --git a/src/index.ts b/src/index.ts index 7db6aa48..ffd24fa1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import {selectionToDOM, anchorInRightPlace, syncNodeSelection} from "./selection import {Decoration, viewDecorations, DecorationSource} from "./decoration" import {DOMObserver} from "./domobserver" import {readDOMChange} from "./domchange" -import {DOMSelection, DOMNode} from "./dom" +import {DOMSelection, DOMNode, isEquivalentPosition, deepActiveElement} from "./dom" import * as browser from "./browser" export {Decoration, DecorationSet, DecorationAttrs, DecorationSource} from "./decoration" @@ -451,8 +451,63 @@ export class EditorView { /// @internal domSelection(): DOMSelection { + if (browser.safari && this.root.nodeType === 11 && deepActiveElement(this.dom.ownerDocument) == this.dom) + return this.safariDomSelection(); + return (this.root as Document).getSelection()! } + + // Used to work around a Safari Selection/shadow DOM bug + // Based on https://github.com/codemirror/dev/issues/414 fix + private safariDomSelection(): DOMSelection { + let found: StaticRange | undefined; + + function read(event: InputEvent) { + event.preventDefault() + event.stopImmediatePropagation() + found = event.getTargetRanges()[0] + } + + // Because Safari (at least in 2018-2022) doesn't provide regular + // access to the selection inside a shadowRoot, we have to perform a + // ridiculous hack to get at it—using `execCommand` to trigger a + // `beforeInput` event so that we can read the target range from the + // event. + this.dom.addEventListener("beforeinput", read, true) + document.execCommand("indent") + this.dom.removeEventListener("beforeinput", read, true) + + let anchorNode = found!.startContainer, anchorOffset = found!.startOffset + let focusNode = found!.endContainer, focusOffset = found!.endOffset + + let currentAnchor = this.domAtPos(this.state.selection.anchor) + + // Since such a range doesn't distinguish between anchor and head, + // use a heuristic that flips it around if its end matches the + // current anchor. + if (isEquivalentPosition(currentAnchor.node, currentAnchor.offset, focusNode, focusOffset)) + [anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset] + + let selection = (this.root as Document).getSelection()! as IndexedDOMSelection + + // Copy methods and bind to the original selection as they could be invoked only on Selection instance + let selectionMethods: Record = {}; + for (const selectionApiKey in selection) { + if (typeof selection[selectionApiKey] === 'function') { + selectionMethods[selectionApiKey] = selection[selectionApiKey].bind(selection); + } + } + + return { + ...selection, // get selection props + ...selectionMethods, // get selection methods + anchorNode, + anchorOffset, + focusNode, + focusOffset, + isCollapsed: !!found?.collapsed, + } + } } function computeDocDeco(view: EditorView) { @@ -758,3 +813,7 @@ export interface DirectEditorProps extends EditorProps { /// the view instance as its `this` binding. dispatchTransaction?: (tr: Transaction) => void } + +interface IndexedDOMSelection extends DOMSelection { + [key: string]: any; +}