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/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 oldRange = sel.getRangeAt(0), oldNode = sel.focusNode, oldOff = sel.focusOffset
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 result = !parentDOM.contains(sel.focusNode!.nodeType == 1 ? sel.focusNode : sel.focusNode!.parentNode) ||
460
- (oldNode == sel.focusNode && oldOff == sel.focusOffset)
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
- sel.removeAllRanges()
463
- sel.addRange(oldRange)
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
  })
@@ -1,6 +1,6 @@
1
1
  import {Selection} from "prosemirror-state"
2
2
  import * as browser from "./browser"
3
- import {domIndex, isEquivalentPosition, selectionCollapsed, DOMSelection} from "./dom"
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: DOMSelection) {
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: DOMSelection) {
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.domSelection()
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.domSelection())
139
+ this.currentSelection.set(this.view.domSelectionRange())
140
140
  }
141
141
 
142
- ignoreSelectionChange(sel: DOMSelection) {
143
- if (sel.rangeCount == 0) return true
144
- let container = sel.getRangeAt(0).commonAncestorContainer
145
- let desc = this.view.docView.nearestDesc(container)
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.nodeType == 3 ? container.parentNode : 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.domSelection()
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.domSelection().focusNode) : null
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.domSelection()) && anchorInRightPlace(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.domSelection().focusNode!
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.domSelection()
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
- sel.collapse(before, before.nodeValue!.length)
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.domSelection()))
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 {selectionCollapsed, isEquivalentPosition, domIndex, isOnEdge} from "./dom"
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.domSelection(), doc = view.state.doc
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.domSelection(), curSel = view.domObserver.currentSelection
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.domSelection()
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.domSelection()
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.domSelection()
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
- // This can crash on Safari if the editor is hidden, and
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 (err) {
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 (!(err instanceof DOMException)) throw err
435
- // declare global: DOMException
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.domSelection()
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