prosemirror-view 1.28.2 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -2
- package/README.md +1 -1
- package/dist/index.cjs +151 -77
- package/dist/index.d.ts +6 -6
- package/dist/index.js +128 -78
- package/package.json +1 -1
- package/src/capturekeys.ts +9 -8
- package/src/clipboard.ts +12 -8
- package/src/dom.ts +14 -8
- package/src/domchange.ts +2 -2
- package/src/domcoords.ts +8 -5
- package/src/domobserver.ts +47 -11
- package/src/index.ts +18 -11
- package/src/input.ts +4 -4
- package/src/selection.ts +6 -6
- package/src/viewdesc.ts +6 -10
package/src/domcoords.ts
CHANGED
|
@@ -452,15 +452,18 @@ function endOfTextblockHorizontal(view: EditorView, state: EditorState, dir: "le
|
|
|
452
452
|
// one character, and see if that moves the cursor out of the
|
|
453
453
|
// textblock (or doesn't move it at all, when at the start/end of
|
|
454
454
|
// the document).
|
|
455
|
-
let
|
|
455
|
+
let {focusNode: oldNode, focusOffset: oldOff, anchorNode, anchorOffset} = view.domSelectionRange()
|
|
456
456
|
let oldBidiLevel = (sel as any).caretBidiLevel // Only for Firefox
|
|
457
457
|
;(sel as any).modify("move", dir, "character")
|
|
458
458
|
let parentDOM = $head.depth ? view.docView.domAfterPos($head.before()) : view.dom
|
|
459
|
-
let
|
|
460
|
-
|
|
459
|
+
let {focusNode: newNode, focusOffset: newOff} = view.domSelectionRange()
|
|
460
|
+
let result = newNode && !parentDOM.contains(newNode.nodeType == 1 ? newNode : newNode.parentNode) ||
|
|
461
|
+
(oldNode == newNode && oldOff == newOff)
|
|
461
462
|
// Restore the previous selection
|
|
462
|
-
|
|
463
|
-
|
|
463
|
+
try {
|
|
464
|
+
sel.collapse(anchorNode, anchorOffset)
|
|
465
|
+
if (oldNode && (oldNode != anchorNode || oldOff != anchorOffset) && sel.extend) sel.extend(oldNode, oldOff)
|
|
466
|
+
} catch (_) {}
|
|
464
467
|
if (oldBidiLevel != null) (sel as any).caretBidiLevel = oldBidiLevel
|
|
465
468
|
return result
|
|
466
469
|
})
|
package/src/domobserver.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Selection} from "prosemirror-state"
|
|
2
2
|
import * as browser from "./browser"
|
|
3
|
-
import {domIndex, isEquivalentPosition, selectionCollapsed,
|
|
3
|
+
import {domIndex, isEquivalentPosition, selectionCollapsed, parentNode, DOMSelectionRange, DOMNode} from "./dom"
|
|
4
4
|
import {hasFocusAndSelection, selectionToDOM, selectionFromDOM} from "./selection"
|
|
5
5
|
import {EditorView} from "./index"
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ class SelectionState {
|
|
|
21
21
|
focusNode: Node | null = null
|
|
22
22
|
focusOffset: number = 0
|
|
23
23
|
|
|
24
|
-
set(sel:
|
|
24
|
+
set(sel: DOMSelectionRange) {
|
|
25
25
|
this.anchorNode = sel.anchorNode; this.anchorOffset = sel.anchorOffset
|
|
26
26
|
this.focusNode = sel.focusNode; this.focusOffset = sel.focusOffset
|
|
27
27
|
}
|
|
@@ -30,7 +30,7 @@ class SelectionState {
|
|
|
30
30
|
this.anchorNode = this.focusNode = null
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
eq(sel:
|
|
33
|
+
eq(sel: DOMSelectionRange) {
|
|
34
34
|
return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset &&
|
|
35
35
|
sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset
|
|
36
36
|
}
|
|
@@ -127,7 +127,7 @@ export class DOMObserver {
|
|
|
127
127
|
// us a selection change event before the DOM changes are
|
|
128
128
|
// reported.
|
|
129
129
|
if (browser.ie && browser.ie_version <= 11 && !this.view.state.selection.empty) {
|
|
130
|
-
let sel = this.view.
|
|
130
|
+
let sel = this.view.domSelectionRange()
|
|
131
131
|
// Selection.isCollapsed isn't reliable on IE
|
|
132
132
|
if (sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode!, sel.anchorOffset))
|
|
133
133
|
return this.flushSoon()
|
|
@@ -136,16 +136,21 @@ export class DOMObserver {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
setCurSelection() {
|
|
139
|
-
this.currentSelection.set(this.view.
|
|
139
|
+
this.currentSelection.set(this.view.domSelectionRange())
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
ignoreSelectionChange(sel:
|
|
143
|
-
if (sel.
|
|
144
|
-
let
|
|
145
|
-
let
|
|
142
|
+
ignoreSelectionChange(sel: DOMSelectionRange) {
|
|
143
|
+
if (!sel.focusNode) return true
|
|
144
|
+
let ancestors: Set<Node> = new Set, container: DOMNode | undefined
|
|
145
|
+
for (let scan: DOMNode | null = sel.focusNode; scan; scan = parentNode(scan)) ancestors.add(scan)
|
|
146
|
+
for (let scan = sel.anchorNode; scan; scan = parentNode(scan)) if (ancestors.has(scan)) {
|
|
147
|
+
container = scan
|
|
148
|
+
break
|
|
149
|
+
}
|
|
150
|
+
let desc = container && this.view.docView.nearestDesc(container)
|
|
146
151
|
if (desc && desc.ignoreMutation({
|
|
147
152
|
type: "selection",
|
|
148
|
-
target: container
|
|
153
|
+
target: container!.nodeType == 3 ? container!.parentNode : container
|
|
149
154
|
} as any)) {
|
|
150
155
|
this.setCurSelection()
|
|
151
156
|
return true
|
|
@@ -161,7 +166,7 @@ export class DOMObserver {
|
|
|
161
166
|
this.queue.length = 0
|
|
162
167
|
}
|
|
163
168
|
|
|
164
|
-
let sel = view.
|
|
169
|
+
let sel = view.domSelectionRange()
|
|
165
170
|
let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel)
|
|
166
171
|
|
|
167
172
|
let from = -1, to = -1, typeOver = false, added: Node[] = []
|
|
@@ -270,3 +275,34 @@ function checkCSS(view: EditorView) {
|
|
|
270
275
|
cssCheckWarned = true
|
|
271
276
|
}
|
|
272
277
|
}
|
|
278
|
+
|
|
279
|
+
// Used to work around a Safari Selection/shadow DOM bug
|
|
280
|
+
// Based on https://github.com/codemirror/dev/issues/414 fix
|
|
281
|
+
export function safariShadowSelectionRange(view: EditorView): DOMSelectionRange {
|
|
282
|
+
let found: StaticRange | undefined
|
|
283
|
+
function read(event: InputEvent) {
|
|
284
|
+
event.preventDefault()
|
|
285
|
+
event.stopImmediatePropagation()
|
|
286
|
+
found = event.getTargetRanges()[0]
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Because Safari (at least in 2018-2022) doesn't provide regular
|
|
290
|
+
// access to the selection inside a shadowRoot, we have to perform a
|
|
291
|
+
// ridiculous hack to get at it—using `execCommand` to trigger a
|
|
292
|
+
// `beforeInput` event so that we can read the target range from the
|
|
293
|
+
// event.
|
|
294
|
+
view.dom.addEventListener("beforeinput", read, true)
|
|
295
|
+
document.execCommand("indent")
|
|
296
|
+
view.dom.removeEventListener("beforeinput", read, true)
|
|
297
|
+
|
|
298
|
+
let anchorNode = found!.startContainer, anchorOffset = found!.startOffset
|
|
299
|
+
let focusNode = found!.endContainer, focusOffset = found!.endOffset
|
|
300
|
+
|
|
301
|
+
let currentAnchor = view.domAtPos(view.state.selection.anchor)
|
|
302
|
+
// Since such a range doesn't distinguish between anchor and head,
|
|
303
|
+
// use a heuristic that flips it around if its end matches the
|
|
304
|
+
// current anchor.
|
|
305
|
+
if (isEquivalentPosition(currentAnchor.node, currentAnchor.offset, focusNode, focusOffset))
|
|
306
|
+
[anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset]
|
|
307
|
+
return {anchorNode, anchorOffset, focusNode, focusOffset}
|
|
308
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,9 +7,9 @@ import {docViewDesc, ViewDesc, NodeView, NodeViewDesc} from "./viewdesc"
|
|
|
7
7
|
import {initInput, destroyInput, dispatchEvent, ensureListeners, clearComposition, InputState} from "./input"
|
|
8
8
|
import {selectionToDOM, anchorInRightPlace, syncNodeSelection} from "./selection"
|
|
9
9
|
import {Decoration, viewDecorations, DecorationSource} from "./decoration"
|
|
10
|
-
import {DOMObserver} from "./domobserver"
|
|
10
|
+
import {DOMObserver, safariShadowSelectionRange} from "./domobserver"
|
|
11
11
|
import {readDOMChange} from "./domchange"
|
|
12
|
-
import {DOMSelection, DOMNode} from "./dom"
|
|
12
|
+
import {DOMSelection, DOMNode, DOMSelectionRange, deepActiveElement} from "./dom"
|
|
13
13
|
import * as browser from "./browser"
|
|
14
14
|
|
|
15
15
|
export {Decoration, DecorationSet, DecorationAttrs, DecorationSource} from "./decoration"
|
|
@@ -195,7 +195,7 @@ export class EditorView {
|
|
|
195
195
|
// Chrome sometimes starts misreporting the selection, so this
|
|
196
196
|
// tracks that and forces a selection reset when our update
|
|
197
197
|
// did write to the node.
|
|
198
|
-
let chromeKludge = browser.chrome ? (this.trackWrites = this.
|
|
198
|
+
let chromeKludge = browser.chrome ? (this.trackWrites = this.domSelectionRange().focusNode) : null
|
|
199
199
|
if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) {
|
|
200
200
|
this.docView.updateOuterDeco([])
|
|
201
201
|
this.docView.destroy()
|
|
@@ -208,7 +208,8 @@ export class EditorView {
|
|
|
208
208
|
// can cause a spurious DOM selection update, disrupting mouse
|
|
209
209
|
// drag selection.
|
|
210
210
|
if (forceSelUpdate ||
|
|
211
|
-
!(this.input.mouseDown && this.domObserver.currentSelection.eq(this.
|
|
211
|
+
!(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) &&
|
|
212
|
+
anchorInRightPlace(this))) {
|
|
212
213
|
selectionToDOM(this, forceSelUpdate)
|
|
213
214
|
} else {
|
|
214
215
|
syncNodeSelection(this, state.selection)
|
|
@@ -230,7 +231,7 @@ export class EditorView {
|
|
|
230
231
|
|
|
231
232
|
/// @internal
|
|
232
233
|
scrollToSelection() {
|
|
233
|
-
let startDOM = this.
|
|
234
|
+
let startDOM = this.domSelectionRange().focusNode!
|
|
234
235
|
if (this.someProp("handleScrollToSelection", f => f(this))) {
|
|
235
236
|
// Handled
|
|
236
237
|
} else if (this.state.selection instanceof NodeSelection) {
|
|
@@ -449,6 +450,12 @@ export class EditorView {
|
|
|
449
450
|
else this.updateState(this.state.apply(tr))
|
|
450
451
|
}
|
|
451
452
|
|
|
453
|
+
/// @internal
|
|
454
|
+
domSelectionRange(): DOMSelectionRange {
|
|
455
|
+
return browser.safari && this.root.nodeType === 11 && deepActiveElement(this.dom.ownerDocument) == this.dom
|
|
456
|
+
? safariShadowSelectionRange(this) : this.domSelection()
|
|
457
|
+
}
|
|
458
|
+
|
|
452
459
|
/// @internal
|
|
453
460
|
domSelection(): DOMSelection {
|
|
454
461
|
return (this.root as Document).getSelection()!
|
|
@@ -631,7 +638,7 @@ export interface EditorProps<P = any> {
|
|
|
631
638
|
|
|
632
639
|
/// Can be used to transform pasted HTML text, _before_ it is parsed,
|
|
633
640
|
/// for example to clean it up.
|
|
634
|
-
transformPastedHTML?: (this: P, html: string) => string
|
|
641
|
+
transformPastedHTML?: (this: P, html: string, view: EditorView) => string
|
|
635
642
|
|
|
636
643
|
/// The [parser](#model.DOMParser) to use when reading content from
|
|
637
644
|
/// the clipboard. When not given, the value of the
|
|
@@ -640,7 +647,7 @@ export interface EditorProps<P = any> {
|
|
|
640
647
|
|
|
641
648
|
/// Transform pasted plain text. The `plain` flag will be true when
|
|
642
649
|
/// the text is pasted as plain text.
|
|
643
|
-
transformPastedText?: (this: P, text: string, plain: boolean) => string
|
|
650
|
+
transformPastedText?: (this: P, text: string, plain: boolean, view: EditorView) => string
|
|
644
651
|
|
|
645
652
|
/// A function to parse text from the clipboard into a document
|
|
646
653
|
/// slice. Called after
|
|
@@ -649,15 +656,15 @@ export interface EditorProps<P = any> {
|
|
|
649
656
|
/// in `<p>` tags, and call
|
|
650
657
|
/// [`clipboardParser`](#view.EditorProps.clipboardParser) on it.
|
|
651
658
|
/// The `plain` flag will be true when the text is pasted as plain text.
|
|
652
|
-
clipboardTextParser?: (this: P, text: string, $context: ResolvedPos, plain: boolean) => Slice
|
|
659
|
+
clipboardTextParser?: (this: P, text: string, $context: ResolvedPos, plain: boolean, view: EditorView) => Slice
|
|
653
660
|
|
|
654
661
|
/// Can be used to transform pasted or dragged-and-dropped content
|
|
655
662
|
/// before it is applied to the document.
|
|
656
|
-
transformPasted?: (this: P, slice: Slice) => Slice
|
|
663
|
+
transformPasted?: (this: P, slice: Slice, view: EditorView) => Slice
|
|
657
664
|
|
|
658
665
|
/// Can be used to transform copied or cut content before it is
|
|
659
666
|
/// serialized to the clipboard.
|
|
660
|
-
transformCopied?: (this: P, slice: Slice) => Slice
|
|
667
|
+
transformCopied?: (this: P, slice: Slice, view: EditorView) => Slice
|
|
661
668
|
|
|
662
669
|
/// Allows you to pass custom rendering and behavior logic for
|
|
663
670
|
/// nodes. Should map node names to constructor functions that
|
|
@@ -704,7 +711,7 @@ export interface EditorProps<P = any> {
|
|
|
704
711
|
/// selection when copying text to the clipboard. By default, the
|
|
705
712
|
/// editor will use [`textBetween`](#model.Node.textBetween) on the
|
|
706
713
|
/// selected range.
|
|
707
|
-
clipboardTextSerializer?: (this: P, content: Slice) => string
|
|
714
|
+
clipboardTextSerializer?: (this: P, content: Slice, view: EditorView) => string
|
|
708
715
|
|
|
709
716
|
/// A set of [document decorations](#view.Decoration) to show in the
|
|
710
717
|
/// view.
|
package/src/input.ts
CHANGED
|
@@ -463,12 +463,12 @@ editHandlers.compositionstart = editHandlers.compositionupdate = view => {
|
|
|
463
463
|
// the inserted text won't inherit the marks. So this moves it
|
|
464
464
|
// inside if necessary.
|
|
465
465
|
if (browser.gecko && state.selection.empty && $pos.parentOffset && !$pos.textOffset && $pos.nodeBefore!.marks.length) {
|
|
466
|
-
let sel = view.
|
|
466
|
+
let sel = view.domSelectionRange()
|
|
467
467
|
for (let node = sel.focusNode, offset = sel.focusOffset; node && node.nodeType == 1 && offset != 0;) {
|
|
468
468
|
let before = offset < 0 ? node.lastChild : node.childNodes[offset - 1]
|
|
469
469
|
if (!before) break
|
|
470
470
|
if (before.nodeType == 3) {
|
|
471
|
-
|
|
471
|
+
view.domSelection().collapse(before, before.nodeValue!.length)
|
|
472
472
|
break
|
|
473
473
|
} else {
|
|
474
474
|
node = before
|
|
@@ -666,7 +666,7 @@ editHandlers.drop = (view, _event) => {
|
|
|
666
666
|
let $mouse = view.state.doc.resolve(eventPos.pos)
|
|
667
667
|
let slice = dragging && dragging.slice
|
|
668
668
|
if (slice) {
|
|
669
|
-
view.someProp("transformPasted", f => { slice = f(slice
|
|
669
|
+
view.someProp("transformPasted", f => { slice = f(slice!, view) })
|
|
670
670
|
} else {
|
|
671
671
|
slice = parseFromClipboard(view, event.dataTransfer.getData(brokenClipboardAPI ? "Text" : "text/plain"),
|
|
672
672
|
brokenClipboardAPI ? null : event.dataTransfer.getData("text/html"), false, $mouse)
|
|
@@ -715,7 +715,7 @@ handlers.focus = view => {
|
|
|
715
715
|
view.domObserver.start()
|
|
716
716
|
view.focused = true
|
|
717
717
|
setTimeout(() => {
|
|
718
|
-
if (view.docView && view.hasFocus() && !view.domObserver.currentSelection.eq(view.
|
|
718
|
+
if (view.docView && view.hasFocus() && !view.domObserver.currentSelection.eq(view.domSelectionRange()))
|
|
719
719
|
selectionToDOM(view)
|
|
720
720
|
}, 20)
|
|
721
721
|
}
|
package/src/selection.ts
CHANGED
|
@@ -2,12 +2,12 @@ import {TextSelection, NodeSelection, Selection} from "prosemirror-state"
|
|
|
2
2
|
import {ResolvedPos} from "prosemirror-model"
|
|
3
3
|
|
|
4
4
|
import * as browser from "./browser"
|
|
5
|
-
import {
|
|
5
|
+
import {isEquivalentPosition, domIndex, isOnEdge, selectionCollapsed} from "./dom"
|
|
6
6
|
import {EditorView} from "./index"
|
|
7
7
|
import {NodeViewDesc} from "./viewdesc"
|
|
8
8
|
|
|
9
9
|
export function selectionFromDOM(view: EditorView, origin: string | null = null) {
|
|
10
|
-
let domSel = view.
|
|
10
|
+
let domSel = view.domSelectionRange(), doc = view.state.doc
|
|
11
11
|
if (!domSel.focusNode) return null
|
|
12
12
|
let nearestDesc = view.docView.nearestDesc(domSel.focusNode), inWidget = nearestDesc && nearestDesc.size == 0
|
|
13
13
|
let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1)
|
|
@@ -50,7 +50,7 @@ export function selectionToDOM(view: EditorView, force = false) {
|
|
|
50
50
|
// in Safari. And the drag selection delay is to workarond issues
|
|
51
51
|
// which only present in Chrome.
|
|
52
52
|
if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && browser.chrome) {
|
|
53
|
-
let domSel = view.
|
|
53
|
+
let domSel = view.domSelectionRange(), curSel = view.domObserver.currentSelection
|
|
54
54
|
if (domSel.anchorNode && curSel.anchorNode &&
|
|
55
55
|
isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset,
|
|
56
56
|
curSel.anchorNode, curSel.anchorOffset)) {
|
|
@@ -121,7 +121,7 @@ function resetEditable(element: HTMLElement) {
|
|
|
121
121
|
function removeClassOnSelectionChange(view: EditorView) {
|
|
122
122
|
let doc = view.dom.ownerDocument
|
|
123
123
|
doc.removeEventListener("selectionchange", view.input.hideSelectionGuard!)
|
|
124
|
-
let domSel = view.
|
|
124
|
+
let domSel = view.domSelectionRange()
|
|
125
125
|
let node = domSel.anchorNode, offset = domSel.anchorOffset
|
|
126
126
|
doc.addEventListener("selectionchange", view.input.hideSelectionGuard = () => {
|
|
127
127
|
if (domSel.anchorNode != node || domSel.anchorOffset != offset) {
|
|
@@ -186,7 +186,7 @@ export function hasFocusAndSelection(view: EditorView) {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
export function hasSelection(view: EditorView) {
|
|
189
|
-
let sel = view.
|
|
189
|
+
let sel = view.domSelectionRange()
|
|
190
190
|
if (!sel.anchorNode) return false
|
|
191
191
|
try {
|
|
192
192
|
// Firefox will raise 'permission denied' errors when accessing
|
|
@@ -201,6 +201,6 @@ export function hasSelection(view: EditorView) {
|
|
|
201
201
|
|
|
202
202
|
export function anchorInRightPlace(view: EditorView) {
|
|
203
203
|
let anchorDOM = view.docView.domFromPos(view.state.selection.anchor, 0)
|
|
204
|
-
let domSel = view.
|
|
204
|
+
let domSel = view.domSelectionRange()
|
|
205
205
|
return isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode!, domSel.anchorOffset)
|
|
206
206
|
}
|
package/src/viewdesc.ts
CHANGED
|
@@ -419,20 +419,16 @@ export class ViewDesc {
|
|
|
419
419
|
if ((domSel.extend || anchor == head) && !brKludge) {
|
|
420
420
|
domSel.collapse(anchorDOM.node, anchorDOM.offset)
|
|
421
421
|
try {
|
|
422
|
-
if (anchor != head)
|
|
423
|
-
|
|
424
|
-
// there was no selection. (#1308)
|
|
425
|
-
try { domSel.extend(headDOM.node, headDOM.offset) }
|
|
426
|
-
catch (_) {}
|
|
427
|
-
}
|
|
422
|
+
if (anchor != head)
|
|
423
|
+
domSel.extend(headDOM.node, headDOM.offset)
|
|
428
424
|
domSelExtended = true
|
|
429
|
-
} catch (
|
|
425
|
+
} catch (_) {
|
|
430
426
|
// In some cases with Chrome the selection is empty after calling
|
|
431
427
|
// collapse, even when it should be valid. This appears to be a bug, but
|
|
432
428
|
// it is difficult to isolate. If this happens fallback to the old path
|
|
433
429
|
// without using extend.
|
|
434
|
-
if
|
|
435
|
-
//
|
|
430
|
+
// Similarly, this could crash on Safari if the editor is hidden, and
|
|
431
|
+
// there was no selection.
|
|
436
432
|
}
|
|
437
433
|
}
|
|
438
434
|
if (!domSelExtended) {
|
|
@@ -762,7 +758,7 @@ export class NodeViewDesc extends ViewDesc {
|
|
|
762
758
|
// are inside of this node
|
|
763
759
|
let {from, to} = view.state.selection
|
|
764
760
|
if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + this.node.content.size) return null
|
|
765
|
-
let sel = view.
|
|
761
|
+
let sel = view.domSelectionRange()
|
|
766
762
|
let textNode = nearbyTextNode(sel.focusNode!, sel.focusOffset)
|
|
767
763
|
if (!textNode || !this.dom.contains(textNode.parentNode)) return null
|
|
768
764
|
|