prosemirror-view 1.33.9 → 1.33.11

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 CHANGED
@@ -1,3 +1,21 @@
1
+ ## 1.33.11 (2024-08-19)
2
+
3
+ ### Bug fixes
4
+
5
+ Delete the selection on composition start when on a mark boundary to force Chrome to apply the correct style to the composition.
6
+
7
+ When the browser starts a composition and the selection spans across blocks, start by clearing it to make sure the composed text node lands in a valid position.
8
+
9
+ ## 1.33.10 (2024-08-13)
10
+
11
+ ### Bug fixes
12
+
13
+ Prevent triple clicks and clicks on atom nodes from dispatching a selection update when the new selection is equal to the current selection.
14
+
15
+ Don't crash when `window.getSelection()` returns null.
16
+
17
+ Create a cursor wrapper when starting a composition with a selection that spans mark boundaries, to avoid odd DOM forms created by Chrome in such a situation.
18
+
1
19
  ## 1.33.9 (2024-07-18)
2
20
 
3
21
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -603,6 +603,7 @@ function endOfTextblockHorizontal(view, state, dir) {
603
603
  atStart = !offset,
604
604
  atEnd = offset == $head.parent.content.size;
605
605
  var sel = view.domSelection();
606
+ if (!sel) return $head.pos == $head.start() || $head.pos == $head.end();
606
607
  if (!maybeRTL.test($head.parent.textContent) || !sel.modify) return dir == "left" || dir == "backward" ? atStart : atEnd;
607
608
  return withFlushedState(view, state, function () {
608
609
  var _view$domSelectionRan = view.domSelectionRange(),
@@ -2214,10 +2215,11 @@ function removeClassOnSelectionChange(view) {
2214
2215
  function selectCursorWrapper(view) {
2215
2216
  var domSel = view.domSelection(),
2216
2217
  range = document.createRange();
2218
+ if (!domSel) return;
2217
2219
  var node = view.cursorWrapper.dom,
2218
2220
  img = node.nodeName == "IMG";
2219
- if (img) range.setEnd(node.parentNode, domIndex(node) + 1);else range.setEnd(node, 0);
2220
- range.collapse(false);
2221
+ if (img) range.setStart(node.parentNode, domIndex(node) + 1);else range.setStart(node, 0);
2222
+ range.collapse(true);
2221
2223
  domSel.removeAllRanges();
2222
2224
  domSel.addRange(range);
2223
2225
  if (!img && !view.state.selection.visible && ie && ie_version <= 11) {
@@ -2449,6 +2451,7 @@ function setSelFocus(view, node, offset) {
2449
2451
  }
2450
2452
  }
2451
2453
  var sel = view.domSelection();
2454
+ if (!sel) return;
2452
2455
  if (selectionCollapsed(sel)) {
2453
2456
  var range = document.createRange();
2454
2457
  range.setEnd(node, offset);
@@ -2969,6 +2972,7 @@ function runHandlerOnContext(view, propName, pos, inside, event) {
2969
2972
  }
2970
2973
  function updateSelection(view, selection, origin) {
2971
2974
  if (!view.focused) view.focus();
2975
+ if (view.state.selection.eq(selection)) return;
2972
2976
  var tr = view.state.tr.setSelection(selection);
2973
2977
  if (origin == "pointer") tr.setMeta("pointer", true);
2974
2978
  view.dispatch(tr);
@@ -3187,22 +3191,23 @@ editHandlers.compositionstart = editHandlers.compositionupdate = function (view)
3187
3191
  if (!view.composing) {
3188
3192
  view.domObserver.flush();
3189
3193
  var state = view.state,
3190
- $pos = state.selection.$from;
3191
- if (state.selection.empty && (state.storedMarks || !$pos.textOffset && $pos.parentOffset && $pos.nodeBefore.marks.some(function (m) {
3194
+ $pos = state.selection.$to;
3195
+ if (state.selection instanceof prosemirrorState.TextSelection && (state.storedMarks || !$pos.textOffset && $pos.parentOffset && $pos.nodeBefore.marks.some(function (m) {
3192
3196
  return m.type.spec.inclusive === false;
3193
3197
  }))) {
3194
3198
  view.markCursor = view.state.storedMarks || $pos.marks();
3195
3199
  endComposition(view, true);
3196
3200
  view.markCursor = null;
3197
3201
  } else {
3198
- endComposition(view);
3202
+ endComposition(view, !state.selection.empty);
3199
3203
  if (gecko && state.selection.empty && $pos.parentOffset && !$pos.textOffset && $pos.nodeBefore.marks.length) {
3200
3204
  var sel = view.domSelectionRange();
3201
3205
  for (var node = sel.focusNode, offset = sel.focusOffset; node && node.nodeType == 1 && offset != 0;) {
3202
3206
  var before = offset < 0 ? node.lastChild : node.childNodes[offset - 1];
3203
3207
  if (!before) break;
3204
3208
  if (before.nodeType == 3) {
3205
- view.domSelection().collapse(before, before.nodeValue.length);
3209
+ var _sel = view.domSelection();
3210
+ if (_sel) _sel.collapse(before, before.nodeValue.length);
3206
3211
  break;
3207
3212
  } else {
3208
3213
  node = before;
@@ -3265,13 +3270,13 @@ function timestampFromCustomEvent() {
3265
3270
  return event.timeStamp;
3266
3271
  }
3267
3272
  function endComposition(view) {
3268
- var forceUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
3273
+ var restarting = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
3269
3274
  if (android && view.domObserver.flushingSoon >= 0) return;
3270
3275
  view.domObserver.forceFlush();
3271
3276
  clearComposition(view);
3272
- if (forceUpdate || view.docView && view.docView.dirty) {
3277
+ if (restarting || view.docView && view.docView.dirty) {
3273
3278
  var sel = selectionFromDOM(view);
3274
- if (sel && !sel.eq(view.state.selection)) view.dispatch(view.state.tr.setSelection(sel));else view.updateState(view.state);
3279
+ if (sel && !sel.eq(view.state.selection)) view.dispatch(view.state.tr.setSelection(sel));else if ((view.markCursor || restarting) && !view.state.selection.empty) view.dispatch(view.state.tr.deleteSelection());else view.updateState(view.state);
3275
3280
  return true;
3276
3281
  }
3277
3282
  return false;
@@ -4562,9 +4567,9 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4562
4567
  };
4563
4568
  } else {
4564
4569
  if (parse.sel) {
4565
- var _sel = resolveSelection(view, view.state.doc, parse.sel);
4566
- if (_sel && !_sel.eq(view.state.selection)) {
4567
- var _tr2 = view.state.tr.setSelection(_sel);
4570
+ var _sel2 = resolveSelection(view, view.state.doc, parse.sel);
4571
+ if (_sel2 && !_sel2.eq(view.state.selection)) {
4572
+ var _tr2 = view.state.tr.setSelection(_sel2);
4568
4573
  if (compositionID) _tr2.setMeta("composition", compositionID);
4569
4574
  view.dispatch(_tr2);
4570
4575
  }
@@ -4641,8 +4646,8 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4641
4646
  }
4642
4647
  if (!tr) tr = view.state.tr.replace(chFrom, chTo, parse.doc.slice(change.start - parse.from, change.endB - parse.from));
4643
4648
  if (parse.sel) {
4644
- var _sel2 = resolveSelection(view, tr.doc, parse.sel);
4645
- if (_sel2 && !(chrome && android && view.composing && _sel2.empty && (change.start != change.endB || view.input.lastAndroidDelete < Date.now() - 100) && (_sel2.head == chFrom || _sel2.head == tr.mapping.map(chTo) - 1) || ie && _sel2.empty && _sel2.head == chFrom)) tr.setSelection(_sel2);
4649
+ var _sel3 = resolveSelection(view, tr.doc, parse.sel);
4650
+ if (_sel3 && !(chrome && android && view.composing && _sel3.empty && (change.start != change.endB || view.input.lastAndroidDelete < Date.now() - 100) && (_sel3.head == chFrom || _sel3.head == tr.mapping.map(chTo) - 1) || ie && _sel3.empty && _sel3.head == chFrom)) tr.setSelection(_sel3);
4646
4651
  }
4647
4652
  if (storedMarks) tr.ensureMarks(storedMarks);
4648
4653
  if (compositionID) tr.setMeta("composition", compositionID);
@@ -5096,6 +5101,12 @@ var EditorView = function () {
5096
5101
  key: "domSelectionRange",
5097
5102
  value: function domSelectionRange() {
5098
5103
  var sel = this.domSelection();
5104
+ if (!sel) return {
5105
+ focusNode: null,
5106
+ focusOffset: 0,
5107
+ anchorNode: null,
5108
+ anchorOffset: 0
5109
+ };
5099
5110
  return safari && this.root.nodeType === 11 && deepActiveElement(this.dom.ownerDocument) == this.dom && safariShadowSelectionRange(this, sel) || sel;
5100
5111
  }
5101
5112
  }, {
@@ -5127,7 +5138,7 @@ function updateCursorWrapper(view) {
5127
5138
  dom.setAttribute("alt", "");
5128
5139
  view.cursorWrapper = {
5129
5140
  dom: dom,
5130
- deco: Decoration.widget(view.state.selection.head, dom, {
5141
+ deco: Decoration.widget(view.state.selection.from, dom, {
5131
5142
  raw: true,
5132
5143
  marks: view.markCursor
5133
5144
  })
package/dist/index.js CHANGED
@@ -659,6 +659,8 @@ function endOfTextblockHorizontal(view, state, dir) {
659
659
  return false;
660
660
  let offset = $head.parentOffset, atStart = !offset, atEnd = offset == $head.parent.content.size;
661
661
  let sel = view.domSelection();
662
+ if (!sel)
663
+ return $head.pos == $head.start() || $head.pos == $head.end();
662
664
  // If the textblock is all LTR, or the browser doesn't support
663
665
  // Selection.modify (Edge), fall back to a primitive approach
664
666
  if (!maybeRTL.test($head.parent.textContent) || !sel.modify)
@@ -2281,12 +2283,14 @@ function removeClassOnSelectionChange(view) {
2281
2283
  }
2282
2284
  function selectCursorWrapper(view) {
2283
2285
  let domSel = view.domSelection(), range = document.createRange();
2286
+ if (!domSel)
2287
+ return;
2284
2288
  let node = view.cursorWrapper.dom, img = node.nodeName == "IMG";
2285
2289
  if (img)
2286
- range.setEnd(node.parentNode, domIndex(node) + 1);
2290
+ range.setStart(node.parentNode, domIndex(node) + 1);
2287
2291
  else
2288
- range.setEnd(node, 0);
2289
- range.collapse(false);
2292
+ range.setStart(node, 0);
2293
+ range.collapse(true);
2290
2294
  domSel.removeAllRanges();
2291
2295
  domSel.addRange(range);
2292
2296
  // Kludge to kill 'control selection' in IE11 when selecting an
@@ -2574,6 +2578,8 @@ function setSelFocus(view, node, offset) {
2574
2578
  }
2575
2579
  }
2576
2580
  let sel = view.domSelection();
2581
+ if (!sel)
2582
+ return;
2577
2583
  if (selectionCollapsed(sel)) {
2578
2584
  let range = document.createRange();
2579
2585
  range.setEnd(node, offset);
@@ -3140,6 +3146,8 @@ function runHandlerOnContext(view, propName, pos, inside, event) {
3140
3146
  function updateSelection(view, selection, origin) {
3141
3147
  if (!view.focused)
3142
3148
  view.focus();
3149
+ if (view.state.selection.eq(selection))
3150
+ return;
3143
3151
  let tr = view.state.tr.setSelection(selection);
3144
3152
  if (origin == "pointer")
3145
3153
  tr.setMeta("pointer", true);
@@ -3394,8 +3402,8 @@ const timeoutComposition = android ? 5000 : -1;
3394
3402
  editHandlers.compositionstart = editHandlers.compositionupdate = view => {
3395
3403
  if (!view.composing) {
3396
3404
  view.domObserver.flush();
3397
- let { state } = view, $pos = state.selection.$from;
3398
- if (state.selection.empty &&
3405
+ let { state } = view, $pos = state.selection.$to;
3406
+ if (state.selection instanceof TextSelection &&
3399
3407
  (state.storedMarks ||
3400
3408
  (!$pos.textOffset && $pos.parentOffset && $pos.nodeBefore.marks.some(m => m.type.spec.inclusive === false)))) {
3401
3409
  // Need to wrap the cursor in mark nodes different from the ones in the DOM context
@@ -3404,7 +3412,7 @@ editHandlers.compositionstart = editHandlers.compositionupdate = view => {
3404
3412
  view.markCursor = null;
3405
3413
  }
3406
3414
  else {
3407
- endComposition(view);
3415
+ endComposition(view, !state.selection.empty);
3408
3416
  // In firefox, if the cursor is after but outside a marked node,
3409
3417
  // the inserted text won't inherit the marks. So this moves it
3410
3418
  // inside if necessary.
@@ -3415,7 +3423,9 @@ editHandlers.compositionstart = editHandlers.compositionupdate = view => {
3415
3423
  if (!before)
3416
3424
  break;
3417
3425
  if (before.nodeType == 3) {
3418
- view.domSelection().collapse(before, before.nodeValue.length);
3426
+ let sel = view.domSelection();
3427
+ if (sel)
3428
+ sel.collapse(before, before.nodeValue.length);
3419
3429
  break;
3420
3430
  }
3421
3431
  else {
@@ -3483,15 +3493,17 @@ function timestampFromCustomEvent() {
3483
3493
  /**
3484
3494
  @internal
3485
3495
  */
3486
- function endComposition(view, forceUpdate = false) {
3496
+ function endComposition(view, restarting = false) {
3487
3497
  if (android && view.domObserver.flushingSoon >= 0)
3488
3498
  return;
3489
3499
  view.domObserver.forceFlush();
3490
3500
  clearComposition(view);
3491
- if (forceUpdate || view.docView && view.docView.dirty) {
3501
+ if (restarting || view.docView && view.docView.dirty) {
3492
3502
  let sel = selectionFromDOM(view);
3493
3503
  if (sel && !sel.eq(view.state.selection))
3494
3504
  view.dispatch(view.state.tr.setSelection(sel));
3505
+ else if ((view.markCursor || restarting) && !view.state.selection.empty)
3506
+ view.dispatch(view.state.tr.deleteSelection());
3495
3507
  else
3496
3508
  view.updateState(view.state);
3497
3509
  return true;
@@ -5646,6 +5658,8 @@ class EditorView {
5646
5658
  */
5647
5659
  domSelectionRange() {
5648
5660
  let sel = this.domSelection();
5661
+ if (!sel)
5662
+ return { focusNode: null, focusOffset: 0, anchorNode: null, anchorOffset: 0 };
5649
5663
  return safari && this.root.nodeType === 11 &&
5650
5664
  deepActiveElement(this.dom.ownerDocument) == this.dom && safariShadowSelectionRange(this, sel) || sel;
5651
5665
  }
@@ -5683,7 +5697,7 @@ function updateCursorWrapper(view) {
5683
5697
  dom.className = "ProseMirror-separator";
5684
5698
  dom.setAttribute("mark-placeholder", "true");
5685
5699
  dom.setAttribute("alt", "");
5686
- view.cursorWrapper = { dom, deco: Decoration.widget(view.state.selection.head, dom, { raw: true, marks: view.markCursor }) };
5700
+ view.cursorWrapper = { dom, deco: Decoration.widget(view.state.selection.from, dom, { raw: true, marks: view.markCursor }) };
5687
5701
  }
5688
5702
  else {
5689
5703
  view.cursorWrapper = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.33.9",
3
+ "version": "1.33.11",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -202,6 +202,7 @@ function setSelFocus(view: EditorView, node: Node, offset: number) {
202
202
  }
203
203
 
204
204
  let sel = view.domSelection()
205
+ if (!sel) return
205
206
  if (selectionCollapsed(sel)) {
206
207
  let range = document.createRange()
207
208
  range.setEnd(node, offset)
package/src/domcoords.ts CHANGED
@@ -465,7 +465,8 @@ function endOfTextblockHorizontal(view: EditorView, state: EditorState, dir: "le
465
465
  let {$head} = state.selection
466
466
  if (!$head.parent.isTextblock) return false
467
467
  let offset = $head.parentOffset, atStart = !offset, atEnd = offset == $head.parent.content.size
468
- let sel = view.domSelection()
468
+ let sel: Selection = view.domSelection()!
469
+ if (!sel) return $head.pos == $head.start() || $head.pos == $head.end()
469
470
  // If the textblock is all LTR, or the browser doesn't support
470
471
  // Selection.modify (Edge), fall back to a primitive approach
471
472
  if (!maybeRTL.test($head.parent.textContent) || !(sel as any).modify)
package/src/index.ts CHANGED
@@ -489,13 +489,14 @@ export class EditorView {
489
489
  /// @internal
490
490
  domSelectionRange(): DOMSelectionRange {
491
491
  let sel = this.domSelection()
492
+ if (!sel) return {focusNode: null, focusOffset: 0, anchorNode: null, anchorOffset: 0}
492
493
  return browser.safari && this.root.nodeType === 11 &&
493
494
  deepActiveElement(this.dom.ownerDocument) == this.dom && safariShadowSelectionRange(this, sel) || sel
494
495
  }
495
496
 
496
497
  /// @internal
497
- domSelection(): DOMSelection {
498
- return (this.root as Document).getSelection()!
498
+ domSelection(): DOMSelection | null {
499
+ return (this.root as Document).getSelection()
499
500
  }
500
501
  }
501
502
 
@@ -526,7 +527,7 @@ function updateCursorWrapper(view: EditorView) {
526
527
  dom.className = "ProseMirror-separator"
527
528
  dom.setAttribute("mark-placeholder", "true")
528
529
  dom.setAttribute("alt", "")
529
- view.cursorWrapper = {dom, deco: Decoration.widget(view.state.selection.head,
530
+ view.cursorWrapper = {dom, deco: Decoration.widget(view.state.selection.from,
530
531
  dom, {raw: true, marks: view.markCursor} as any)}
531
532
  } else {
532
533
  view.cursorWrapper = null
package/src/input.ts CHANGED
@@ -183,6 +183,7 @@ function runHandlerOnContext(
183
183
 
184
184
  function updateSelection(view: EditorView, selection: Selection, origin: string) {
185
185
  if (!view.focused) view.focus()
186
+ if (view.state.selection.eq(selection)) return
186
187
  let tr = view.state.tr.setSelection(selection)
187
188
  if (origin == "pointer") tr.setMeta("pointer", true)
188
189
  view.dispatch(tr)
@@ -453,8 +454,8 @@ const timeoutComposition = browser.android ? 5000 : -1
453
454
  editHandlers.compositionstart = editHandlers.compositionupdate = view => {
454
455
  if (!view.composing) {
455
456
  view.domObserver.flush()
456
- let {state} = view, $pos = state.selection.$from
457
- if (state.selection.empty &&
457
+ let {state} = view, $pos = state.selection.$to
458
+ if (state.selection instanceof TextSelection &&
458
459
  (state.storedMarks ||
459
460
  (!$pos.textOffset && $pos.parentOffset && $pos.nodeBefore!.marks.some(m => m.type.spec.inclusive === false)))) {
460
461
  // Need to wrap the cursor in mark nodes different from the ones in the DOM context
@@ -462,7 +463,7 @@ editHandlers.compositionstart = editHandlers.compositionupdate = view => {
462
463
  endComposition(view, true)
463
464
  view.markCursor = null
464
465
  } else {
465
- endComposition(view)
466
+ endComposition(view, !state.selection.empty)
466
467
  // In firefox, if the cursor is after but outside a marked node,
467
468
  // the inserted text won't inherit the marks. So this moves it
468
469
  // inside if necessary.
@@ -472,7 +473,8 @@ editHandlers.compositionstart = editHandlers.compositionupdate = view => {
472
473
  let before = offset < 0 ? node.lastChild : node.childNodes[offset - 1]
473
474
  if (!before) break
474
475
  if (before.nodeType == 3) {
475
- view.domSelection().collapse(before, before.nodeValue!.length)
476
+ let sel = view.domSelection()
477
+ if (sel) sel.collapse(before, before.nodeValue!.length)
476
478
  break
477
479
  } else {
478
480
  node = before
@@ -537,13 +539,14 @@ function timestampFromCustomEvent() {
537
539
  }
538
540
 
539
541
  /// @internal
540
- export function endComposition(view: EditorView, forceUpdate = false) {
542
+ export function endComposition(view: EditorView, restarting = false) {
541
543
  if (browser.android && view.domObserver.flushingSoon >= 0) return
542
544
  view.domObserver.forceFlush()
543
545
  clearComposition(view)
544
- if (forceUpdate || view.docView && view.docView.dirty) {
546
+ if (restarting || view.docView && view.docView.dirty) {
545
547
  let sel = selectionFromDOM(view)
546
548
  if (sel && !sel.eq(view.state.selection)) view.dispatch(view.state.tr.setSelection(sel))
549
+ else if ((view.markCursor || restarting) && !view.state.selection.empty) view.dispatch(view.state.tr.deleteSelection())
547
550
  else view.updateState(view.state)
548
551
  return true
549
552
  }
package/src/selection.ts CHANGED
@@ -136,10 +136,11 @@ function removeClassOnSelectionChange(view: EditorView) {
136
136
 
137
137
  function selectCursorWrapper(view: EditorView) {
138
138
  let domSel = view.domSelection(), range = document.createRange()
139
+ if (!domSel) return
139
140
  let node = view.cursorWrapper!.dom, img = node.nodeName == "IMG"
140
- if (img) range.setEnd(node.parentNode!, domIndex(node) + 1)
141
- else range.setEnd(node, 0)
142
- range.collapse(false)
141
+ if (img) range.setStart(node.parentNode!, domIndex(node) + 1)
142
+ else range.setStart(node, 0)
143
+ range.collapse(true)
143
144
  domSel.removeAllRanges()
144
145
  domSel.addRange(range)
145
146
  // Kludge to kill 'control selection' in IE11 when selecting an