@codemirror/view 6.3.0 → 6.3.1

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/dist/index.js CHANGED
@@ -871,7 +871,7 @@ class WidgetView extends ContentView {
871
871
  if (pos > 0 ? i == 0 : i == rects.length - 1 || rect.top < rect.bottom)
872
872
  break;
873
873
  }
874
- return (pos == 0 && side > 0 || pos == this.length && side <= 0) ? rect : flattenRect(rect, pos == 0);
874
+ return flattenRect(rect, this.side > 0);
875
875
  }
876
876
  get isEditable() { return false; }
877
877
  destroy() {
@@ -1024,7 +1024,6 @@ function inlineDOMAtPos(parent, pos) {
1024
1024
  break;
1025
1025
  off = end;
1026
1026
  }
1027
- // if (i) return DOMPos.after(children[i - 1].dom!)
1028
1027
  for (let j = i; j > 0; j--) {
1029
1028
  let prev = children[j - 1];
1030
1029
  if (prev.dom.parentNode == dom)
@@ -1051,21 +1050,59 @@ function joinInlineInto(parent, view, open) {
1051
1050
  parent.length += view.length;
1052
1051
  }
1053
1052
  function coordsInChildren(view, pos, side) {
1054
- for (let off = 0, i = 0; i < view.children.length; i++) {
1055
- let child = view.children[i], end = off + child.length, next;
1056
- if ((side <= 0 || end == view.length || child.getSide() > 0 ? end >= pos : end > pos) &&
1057
- (pos < end || i + 1 == view.children.length || (next = view.children[i + 1]).length || next.getSide() > 0)) {
1058
- let flatten = 0;
1059
- if (end == off) {
1060
- if (child.getSide() <= 0)
1061
- continue;
1062
- flatten = side = -child.getSide();
1053
+ if (!view.children.length)
1054
+ return fallbackRect(view);
1055
+ return (side <= 0 ? coordsInChildrenBefore : coordsInChildrenAfter)(view, pos);
1056
+ }
1057
+ function coordsInChildrenBefore(view, pos) {
1058
+ // Find the last leaf in the tree that touches pos and doesn't have getSide() > 0
1059
+ let found = null, foundPos = -1;
1060
+ function scan(view, pos) {
1061
+ for (let i = 0, off = 0; i < view.children.length && off <= pos; i++) {
1062
+ let child = view.children[i], end = off + child.length;
1063
+ if (end >= pos) {
1064
+ if (child.children.length) {
1065
+ if (scan(child, pos - off))
1066
+ return true;
1067
+ }
1068
+ else if (end >= pos) {
1069
+ if (end == pos && child.getSide() > 0)
1070
+ return true;
1071
+ found = child;
1072
+ foundPos = pos - off;
1073
+ }
1074
+ }
1075
+ off = end;
1076
+ }
1077
+ }
1078
+ scan(view, pos);
1079
+ return found ? found.coordsAt(Math.max(0, foundPos), -1) : coordsInChildrenAfter(view, pos);
1080
+ }
1081
+ function coordsInChildrenAfter(view, pos) {
1082
+ // Find the first leaf in the tree that touches pos and doesn't have getSide() < 0
1083
+ let found = null, foundPos = -1;
1084
+ function scan(view, pos) {
1085
+ for (let i = view.children.length - 1, off = view.length; i >= 0 && off >= pos; i--) {
1086
+ let child = view.children[i];
1087
+ off -= child.length;
1088
+ if (off <= pos) {
1089
+ if (child.children.length) {
1090
+ if (scan(child, pos - off))
1091
+ return true;
1092
+ }
1093
+ else if (off <= pos) {
1094
+ if (off == pos && child.getSide() < 0)
1095
+ return true;
1096
+ found = child;
1097
+ foundPos = pos - off;
1098
+ }
1063
1099
  }
1064
- let rect = child.coordsAt(Math.max(0, pos - off), side);
1065
- return flatten && rect ? flattenRect(rect, side < 0) : rect;
1066
1100
  }
1067
- off = end;
1068
1101
  }
1102
+ scan(view, pos);
1103
+ return found ? found.coordsAt(Math.max(0, foundPos), 1) : coordsInChildrenBefore(view, pos);
1104
+ }
1105
+ function fallbackRect(view) {
1069
1106
  let last = view.dom.lastChild;
1070
1107
  if (!last)
1071
1108
  return view.dom.getBoundingClientRect();
@@ -1683,7 +1720,7 @@ class ContentBuilder {
1683
1720
  this.addBlockWidget(new BlockWidgetView(deco.widget || new NullWidget("div"), len, type));
1684
1721
  }
1685
1722
  else {
1686
- let view = WidgetView.create(deco.widget || new NullWidget("span"), len, deco.startSide);
1723
+ let view = WidgetView.create(deco.widget || new NullWidget("span"), len, len ? 0 : deco.startSide);
1687
1724
  let cursorBefore = this.atCursorPos && !view.isEditable && openStart <= active.length && (from < to || deco.startSide > 0);
1688
1725
  let cursorAfter = !view.isEditable && (from < to || deco.startSide <= 0);
1689
1726
  let line = this.getLine();
@@ -4876,7 +4913,7 @@ class ViewState {
4876
4913
  refresh = true;
4877
4914
  if (refresh || oracle.lineWrapping && Math.abs(contentWidth - this.contentDOMWidth) > oracle.charWidth) {
4878
4915
  let { lineHeight, charWidth } = view.docView.measureTextSize();
4879
- refresh = oracle.refresh(whiteSpace, lineHeight, charWidth, contentWidth / charWidth, lineHeights);
4916
+ refresh = lineHeight > 0 && oracle.refresh(whiteSpace, lineHeight, charWidth, contentWidth / charWidth, lineHeights);
4880
4917
  if (refresh) {
4881
4918
  view.docView.minWidth = 0;
4882
4919
  result |= 8 /* UpdateFlag.Geometry */;
@@ -5020,8 +5057,10 @@ class ViewState {
5020
5057
  let marginHeight = (margin / this.heightOracle.lineLength) * this.heightOracle.lineHeight;
5021
5058
  let top, bot;
5022
5059
  if (target != null) {
5023
- top = Math.max(line.from, target - margin);
5024
- bot = Math.min(line.to, target + margin);
5060
+ let targetFrac = findFraction(structure, target);
5061
+ let spaceFrac = ((this.visibleBottom - this.visibleTop) / 2 + marginHeight) / line.height;
5062
+ top = targetFrac - spaceFrac;
5063
+ bot = targetFrac + spaceFrac;
5025
5064
  }
5026
5065
  else {
5027
5066
  top = (this.visibleTop - line.top - marginHeight) / line.height;
@@ -5031,14 +5070,16 @@ class ViewState {
5031
5070
  viewTo = findPosition(structure, bot);
5032
5071
  }
5033
5072
  else {
5073
+ let totalWidth = structure.total * this.heightOracle.charWidth;
5074
+ let marginWidth = margin * this.heightOracle.charWidth;
5034
5075
  let left, right;
5035
5076
  if (target != null) {
5036
- left = Math.max(line.from, target - doubleMargin);
5037
- right = Math.min(line.to, target + doubleMargin);
5077
+ let targetFrac = findFraction(structure, target);
5078
+ let spaceFrac = ((this.pixelViewport.right - this.pixelViewport.left) / 2 + marginWidth) / totalWidth;
5079
+ left = targetFrac - spaceFrac;
5080
+ right = targetFrac + spaceFrac;
5038
5081
  }
5039
5082
  else {
5040
- let totalWidth = structure.total * this.heightOracle.charWidth;
5041
- let marginWidth = margin * this.heightOracle.charWidth;
5042
5083
  left = (this.pixelViewport.left - marginWidth) / totalWidth;
5043
5084
  right = (this.pixelViewport.right + marginWidth) / totalWidth;
5044
5085
  }
@@ -5445,6 +5486,227 @@ const baseTheme$1 = /*@__PURE__*/buildTheme("." + baseThemeID, {
5445
5486
  }
5446
5487
  }, lightDarkIDs);
5447
5488
 
5489
+ class DOMChange {
5490
+ constructor(view, start, end, typeOver) {
5491
+ this.typeOver = typeOver;
5492
+ this.bounds = null;
5493
+ this.text = "";
5494
+ let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
5495
+ if (start > -1 && !view.state.readOnly && (this.bounds = view.docView.domBoundsAround(start, end, 0))) {
5496
+ let selPoints = iHead || iAnchor ? [] : selectionPoints(view);
5497
+ let reader = new DOMReader(selPoints, view.state);
5498
+ reader.readRange(this.bounds.startDOM, this.bounds.endDOM);
5499
+ this.text = reader.text;
5500
+ this.newSel = selectionFromPoints(selPoints, this.bounds.from);
5501
+ }
5502
+ else {
5503
+ let domSel = view.observer.selectionRange;
5504
+ let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ||
5505
+ !contains(view.contentDOM, domSel.focusNode)
5506
+ ? view.state.selection.main.head
5507
+ : view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
5508
+ let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset ||
5509
+ !contains(view.contentDOM, domSel.anchorNode)
5510
+ ? view.state.selection.main.anchor
5511
+ : view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
5512
+ this.newSel = EditorSelection.single(anchor, head);
5513
+ }
5514
+ }
5515
+ }
5516
+ function applyDOMChange(view, domChange) {
5517
+ let change;
5518
+ let { newSel } = domChange, sel = view.state.selection.main;
5519
+ if (domChange.bounds) {
5520
+ let { from, to } = domChange.bounds;
5521
+ let preferredPos = sel.from, preferredSide = null;
5522
+ // Prefer anchoring to end when Backspace is pressed (or, on
5523
+ // Android, when something was deleted)
5524
+ if (view.inputState.lastKeyCode === 8 && view.inputState.lastKeyTime > Date.now() - 100 ||
5525
+ browser.android && domChange.text.length < to - from) {
5526
+ preferredPos = sel.to;
5527
+ preferredSide = "end";
5528
+ }
5529
+ let diff = findDiff(view.state.doc.sliceString(from, to, LineBreakPlaceholder), domChange.text, preferredPos - from, preferredSide);
5530
+ if (diff) {
5531
+ // Chrome inserts two newlines when pressing shift-enter at the
5532
+ // end of a line. DomChange drops one of those.
5533
+ if (browser.chrome && view.inputState.lastKeyCode == 13 &&
5534
+ diff.toB == diff.from + 2 && domChange.text.slice(diff.from, diff.toB) == LineBreakPlaceholder + LineBreakPlaceholder)
5535
+ diff.toB--;
5536
+ change = { from: from + diff.from, to: from + diff.toA,
5537
+ insert: Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
5538
+ }
5539
+ }
5540
+ else if (newSel && (!view.hasFocus || !view.state.facet(editable) || newSel.main.eq(sel))) {
5541
+ newSel = null;
5542
+ }
5543
+ if (!change && !newSel)
5544
+ return false;
5545
+ if (!change && domChange.typeOver && !sel.empty && newSel && newSel.main.empty) {
5546
+ // Heuristic to notice typing over a selected character
5547
+ change = { from: sel.from, to: sel.to, insert: view.state.doc.slice(sel.from, sel.to) };
5548
+ }
5549
+ else if (change && change.from >= sel.from && change.to <= sel.to &&
5550
+ (change.from != sel.from || change.to != sel.to) &&
5551
+ (sel.to - sel.from) - (change.to - change.from) <= 4) {
5552
+ // If the change is inside the selection and covers most of it,
5553
+ // assume it is a selection replace (with identical characters at
5554
+ // the start/end not included in the diff)
5555
+ change = {
5556
+ from: sel.from, to: sel.to,
5557
+ insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
5558
+ };
5559
+ }
5560
+ else if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
5561
+ /^\. ?$/.test(change.insert.toString())) {
5562
+ // Detect insert-period-on-double-space Mac and Android behavior,
5563
+ // and transform it into a regular space insert.
5564
+ if (newSel && change.insert.length == 2)
5565
+ newSel = EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
5566
+ change = { from: sel.from, to: sel.to, insert: Text.of([" "]) };
5567
+ }
5568
+ if (change) {
5569
+ let startState = view.state;
5570
+ if (browser.ios && view.inputState.flushIOSKey(view))
5571
+ return true;
5572
+ // Android browsers don't fire reasonable key events for enter,
5573
+ // backspace, or delete. So this detects changes that look like
5574
+ // they're caused by those keys, and reinterprets them as key
5575
+ // events. (Some of these keys are also handled by beforeinput
5576
+ // events and the pendingAndroidKey mechanism, but that's not
5577
+ // reliable in all situations.)
5578
+ if (browser.android &&
5579
+ ((change.from == sel.from && change.to == sel.to &&
5580
+ change.insert.length == 1 && change.insert.lines == 2 &&
5581
+ dispatchKey(view.contentDOM, "Enter", 13)) ||
5582
+ (change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 &&
5583
+ dispatchKey(view.contentDOM, "Backspace", 8)) ||
5584
+ (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
5585
+ dispatchKey(view.contentDOM, "Delete", 46))))
5586
+ return true;
5587
+ let text = change.insert.toString();
5588
+ if (view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text)))
5589
+ return true;
5590
+ if (view.inputState.composing >= 0)
5591
+ view.inputState.composing++;
5592
+ let tr;
5593
+ if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
5594
+ (!newSel || newSel.main.empty && newSel.main.from == change.from + change.insert.length) &&
5595
+ view.inputState.composing < 0) {
5596
+ let before = sel.from < change.from ? startState.sliceDoc(sel.from, change.from) : "";
5597
+ let after = sel.to > change.to ? startState.sliceDoc(change.to, sel.to) : "";
5598
+ tr = startState.replaceSelection(view.state.toText(before + change.insert.sliceString(0, undefined, view.state.lineBreak) + after));
5599
+ }
5600
+ else {
5601
+ let changes = startState.changes(change);
5602
+ let mainSel = newSel && !startState.selection.main.eq(newSel.main) && newSel.main.to <= changes.newLength
5603
+ ? newSel.main : undefined;
5604
+ // Try to apply a composition change to all cursors
5605
+ if (startState.selection.ranges.length > 1 && view.inputState.composing >= 0 &&
5606
+ change.to <= sel.to && change.to >= sel.to - 10) {
5607
+ let replaced = view.state.sliceDoc(change.from, change.to);
5608
+ let compositionRange = compositionSurroundingNode(view) || view.state.doc.lineAt(sel.head);
5609
+ let offset = sel.to - change.to, size = sel.to - sel.from;
5610
+ tr = startState.changeByRange(range => {
5611
+ if (range.from == sel.from && range.to == sel.to)
5612
+ return { changes, range: mainSel || range.map(changes) };
5613
+ let to = range.to - offset, from = to - replaced.length;
5614
+ if (range.to - range.from != size || view.state.sliceDoc(from, to) != replaced ||
5615
+ // Unfortunately, there's no way to make multiple
5616
+ // changes in the same node work without aborting
5617
+ // composition, so cursors in the composition range are
5618
+ // ignored.
5619
+ compositionRange && range.to >= compositionRange.from && range.from <= compositionRange.to)
5620
+ return { range };
5621
+ let rangeChanges = startState.changes({ from, to, insert: change.insert }), selOff = range.to - sel.to;
5622
+ return {
5623
+ changes: rangeChanges,
5624
+ range: !mainSel ? range.map(rangeChanges) :
5625
+ EditorSelection.range(Math.max(0, mainSel.anchor + selOff), Math.max(0, mainSel.head + selOff))
5626
+ };
5627
+ });
5628
+ }
5629
+ else {
5630
+ tr = {
5631
+ changes,
5632
+ selection: mainSel && startState.selection.replaceRange(mainSel)
5633
+ };
5634
+ }
5635
+ }
5636
+ let userEvent = "input.type";
5637
+ if (view.composing) {
5638
+ userEvent += ".compose";
5639
+ if (view.inputState.compositionFirstChange) {
5640
+ userEvent += ".start";
5641
+ view.inputState.compositionFirstChange = false;
5642
+ }
5643
+ }
5644
+ view.dispatch(tr, { scrollIntoView: true, userEvent });
5645
+ return true;
5646
+ }
5647
+ else if (newSel && !newSel.main.eq(sel)) {
5648
+ let scrollIntoView = false, userEvent = "select";
5649
+ if (view.inputState.lastSelectionTime > Date.now() - 50) {
5650
+ if (view.inputState.lastSelectionOrigin == "select")
5651
+ scrollIntoView = true;
5652
+ userEvent = view.inputState.lastSelectionOrigin;
5653
+ }
5654
+ view.dispatch({ selection: newSel, scrollIntoView, userEvent });
5655
+ return true;
5656
+ }
5657
+ else {
5658
+ return false;
5659
+ }
5660
+ }
5661
+ function findDiff(a, b, preferredPos, preferredSide) {
5662
+ let minLen = Math.min(a.length, b.length);
5663
+ let from = 0;
5664
+ while (from < minLen && a.charCodeAt(from) == b.charCodeAt(from))
5665
+ from++;
5666
+ if (from == minLen && a.length == b.length)
5667
+ return null;
5668
+ let toA = a.length, toB = b.length;
5669
+ while (toA > 0 && toB > 0 && a.charCodeAt(toA - 1) == b.charCodeAt(toB - 1)) {
5670
+ toA--;
5671
+ toB--;
5672
+ }
5673
+ if (preferredSide == "end") {
5674
+ let adjust = Math.max(0, from - Math.min(toA, toB));
5675
+ preferredPos -= toA + adjust - from;
5676
+ }
5677
+ if (toA < from && a.length < b.length) {
5678
+ let move = preferredPos <= from && preferredPos >= toA ? from - preferredPos : 0;
5679
+ from -= move;
5680
+ toB = from + (toB - toA);
5681
+ toA = from;
5682
+ }
5683
+ else if (toB < from) {
5684
+ let move = preferredPos <= from && preferredPos >= toB ? from - preferredPos : 0;
5685
+ from -= move;
5686
+ toA = from + (toA - toB);
5687
+ toB = from;
5688
+ }
5689
+ return { from, toA, toB };
5690
+ }
5691
+ function selectionPoints(view) {
5692
+ let result = [];
5693
+ if (view.root.activeElement != view.contentDOM)
5694
+ return result;
5695
+ let { anchorNode, anchorOffset, focusNode, focusOffset } = view.observer.selectionRange;
5696
+ if (anchorNode) {
5697
+ result.push(new DOMPoint(anchorNode, anchorOffset));
5698
+ if (focusNode != anchorNode || focusOffset != anchorOffset)
5699
+ result.push(new DOMPoint(focusNode, focusOffset));
5700
+ }
5701
+ return result;
5702
+ }
5703
+ function selectionFromPoints(points, base) {
5704
+ if (points.length == 0)
5705
+ return null;
5706
+ let anchor = points[0].pos, head = points.length == 2 ? points[1].pos : anchor;
5707
+ return anchor > -1 && head > -1 ? EditorSelection.single(anchor + base, head + base) : null;
5708
+ }
5709
+
5448
5710
  const observeOptions = {
5449
5711
  childList: true,
5450
5712
  characterData: true,
@@ -5456,10 +5718,8 @@ const observeOptions = {
5456
5718
  // DOMCharacterDataModified there
5457
5719
  const useCharData = browser.ie && browser.ie_version <= 11;
5458
5720
  class DOMObserver {
5459
- constructor(view, onChange, onScrollChanged) {
5721
+ constructor(view) {
5460
5722
  this.view = view;
5461
- this.onChange = onChange;
5462
- this.onScrollChanged = onScrollChanged;
5463
5723
  this.active = false;
5464
5724
  // The known selection. Kept in our own object, as opposed to just
5465
5725
  // directly accessing the selection because:
@@ -5474,6 +5734,7 @@ class DOMObserver {
5474
5734
  this.resizeTimeout = -1;
5475
5735
  this.queue = [];
5476
5736
  this.delayedAndroidKey = null;
5737
+ this.flushingAndroidKey = -1;
5477
5738
  this.lastChange = 0;
5478
5739
  this.scrollTargets = [];
5479
5740
  this.intersection = null;
@@ -5542,6 +5803,11 @@ class DOMObserver {
5542
5803
  this.listenForScroll();
5543
5804
  this.readSelectionRange();
5544
5805
  }
5806
+ onScrollChanged(e) {
5807
+ this.view.inputState.runScrollHandlers(this.view, e);
5808
+ if (this.intersecting)
5809
+ this.view.measure();
5810
+ }
5545
5811
  onScroll(e) {
5546
5812
  if (this.intersecting)
5547
5813
  this.flush(false);
@@ -5701,14 +5967,17 @@ class DOMObserver {
5701
5967
  // them or, if that has no effect, dispatches the given key.
5702
5968
  delayAndroidKey(key, keyCode) {
5703
5969
  var _a;
5704
- if (!this.delayedAndroidKey)
5705
- this.view.win.requestAnimationFrame(() => {
5970
+ if (!this.delayedAndroidKey) {
5971
+ let flush = () => {
5706
5972
  let key = this.delayedAndroidKey;
5707
- this.delayedAndroidKey = null;
5708
- this.delayedFlush = -1;
5709
- if (!this.flush() && key.force)
5710
- dispatchKey(this.dom, key.key, key.keyCode);
5711
- });
5973
+ if (key) {
5974
+ this.clearDelayedAndroidKey();
5975
+ if (!this.flush() && key.force)
5976
+ dispatchKey(this.dom, key.key, key.keyCode);
5977
+ }
5978
+ };
5979
+ this.flushingAndroidKey = this.view.win.requestAnimationFrame(flush);
5980
+ }
5712
5981
  // Since backspace beforeinput is sometimes signalled spuriously,
5713
5982
  // Enter always takes precedence.
5714
5983
  if (!this.delayedAndroidKey || key == "Enter")
@@ -5721,6 +5990,11 @@ class DOMObserver {
5721
5990
  force: this.lastChange < Date.now() - 50 || !!((_a = this.delayedAndroidKey) === null || _a === void 0 ? void 0 : _a.force)
5722
5991
  };
5723
5992
  }
5993
+ clearDelayedAndroidKey() {
5994
+ this.win.cancelAnimationFrame(this.flushingAndroidKey);
5995
+ this.delayedAndroidKey = null;
5996
+ this.flushingAndroidKey = -1;
5997
+ }
5724
5998
  flushSoon() {
5725
5999
  if (this.delayedFlush < 0)
5726
6000
  this.delayedFlush = this.view.win.requestAnimationFrame(() => { this.delayedFlush = -1; this.flush(); });
@@ -5755,6 +6029,17 @@ class DOMObserver {
5755
6029
  }
5756
6030
  return { from, to, typeOver };
5757
6031
  }
6032
+ readChange() {
6033
+ let { from, to, typeOver } = this.processRecords();
6034
+ let newSel = this.selectionChanged && hasSelection(this.dom, this.selectionRange);
6035
+ if (from < 0 && !newSel)
6036
+ return null;
6037
+ if (from > -1)
6038
+ this.lastChange = Date.now();
6039
+ this.view.inputState.lastFocusTime = 0;
6040
+ this.selectionChanged = false;
6041
+ return new DOMChange(this.view, from, to, typeOver);
6042
+ }
5758
6043
  // Apply pending changes, if any
5759
6044
  flush(readSelection = true) {
5760
6045
  // Completely hold off flushing when pending keys are set—the code
@@ -5764,16 +6049,11 @@ class DOMObserver {
5764
6049
  return false;
5765
6050
  if (readSelection)
5766
6051
  this.readSelectionRange();
5767
- let { from, to, typeOver } = this.processRecords();
5768
- let newSel = this.selectionChanged && hasSelection(this.dom, this.selectionRange);
5769
- if (from < 0 && !newSel)
6052
+ let domChange = this.readChange();
6053
+ if (!domChange)
5770
6054
  return false;
5771
- if (from > -1)
5772
- this.lastChange = Date.now();
5773
- this.view.inputState.lastFocusTime = 0;
5774
- this.selectionChanged = false;
5775
6055
  let startState = this.view.state;
5776
- let handled = this.onChange(from, to, typeOver);
6056
+ let handled = applyDOMChange(this.view, domChange);
5777
6057
  // The view wasn't updated
5778
6058
  if (this.view.state == startState)
5779
6059
  this.view.update([]);
@@ -5829,6 +6109,8 @@ class DOMObserver {
5829
6109
  this.removeWindowListeners(this.win);
5830
6110
  clearTimeout(this.parentCheck);
5831
6111
  clearTimeout(this.resizeTimeout);
6112
+ this.win.cancelAnimationFrame(this.delayedFlush);
6113
+ this.win.cancelAnimationFrame(this.flushingAndroidKey);
5832
6114
  }
5833
6115
  }
5834
6116
  function findChild(cView, dom, dir) {
@@ -5870,218 +6152,6 @@ function safariSelectionRangeHack(view) {
5870
6152
  return { anchorNode, anchorOffset, focusNode, focusOffset };
5871
6153
  }
5872
6154
 
5873
- function applyDOMChange(view, start, end, typeOver) {
5874
- let change, newSel;
5875
- let sel = view.state.selection.main;
5876
- if (start > -1) {
5877
- let bounds = view.docView.domBoundsAround(start, end, 0);
5878
- if (!bounds || view.state.readOnly)
5879
- return false;
5880
- let { from, to } = bounds;
5881
- let selPoints = view.docView.impreciseHead || view.docView.impreciseAnchor ? [] : selectionPoints(view);
5882
- let reader = new DOMReader(selPoints, view.state);
5883
- reader.readRange(bounds.startDOM, bounds.endDOM);
5884
- let preferredPos = sel.from, preferredSide = null;
5885
- // Prefer anchoring to end when Backspace is pressed (or, on
5886
- // Android, when something was deleted)
5887
- if (view.inputState.lastKeyCode === 8 && view.inputState.lastKeyTime > Date.now() - 100 ||
5888
- browser.android && reader.text.length < to - from) {
5889
- preferredPos = sel.to;
5890
- preferredSide = "end";
5891
- }
5892
- let diff = findDiff(view.state.doc.sliceString(from, to, LineBreakPlaceholder), reader.text, preferredPos - from, preferredSide);
5893
- if (diff) {
5894
- // Chrome inserts two newlines when pressing shift-enter at the
5895
- // end of a line. This drops one of those.
5896
- if (browser.chrome && view.inputState.lastKeyCode == 13 &&
5897
- diff.toB == diff.from + 2 && reader.text.slice(diff.from, diff.toB) == LineBreakPlaceholder + LineBreakPlaceholder)
5898
- diff.toB--;
5899
- change = { from: from + diff.from, to: from + diff.toA,
5900
- insert: Text.of(reader.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
5901
- }
5902
- newSel = selectionFromPoints(selPoints, from);
5903
- }
5904
- else if (view.hasFocus || !view.state.facet(editable)) {
5905
- let domSel = view.observer.selectionRange;
5906
- let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
5907
- let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ||
5908
- !contains(view.contentDOM, domSel.focusNode)
5909
- ? view.state.selection.main.head
5910
- : view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
5911
- let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset ||
5912
- !contains(view.contentDOM, domSel.anchorNode)
5913
- ? view.state.selection.main.anchor
5914
- : view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
5915
- if (head != sel.head || anchor != sel.anchor)
5916
- newSel = EditorSelection.single(anchor, head);
5917
- }
5918
- if (!change && !newSel)
5919
- return false;
5920
- if (!change && typeOver && !sel.empty && newSel && newSel.main.empty) {
5921
- // Heuristic to notice typing over a selected character
5922
- change = { from: sel.from, to: sel.to, insert: view.state.doc.slice(sel.from, sel.to) };
5923
- }
5924
- else if (change && change.from >= sel.from && change.to <= sel.to &&
5925
- (change.from != sel.from || change.to != sel.to) &&
5926
- (sel.to - sel.from) - (change.to - change.from) <= 4) {
5927
- // If the change is inside the selection and covers most of it,
5928
- // assume it is a selection replace (with identical characters at
5929
- // the start/end not included in the diff)
5930
- change = {
5931
- from: sel.from, to: sel.to,
5932
- insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
5933
- };
5934
- }
5935
- else if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
5936
- /^\. ?$/.test(change.insert.toString())) {
5937
- // Detect insert-period-on-double-space Mac and Android behavior,
5938
- // and transform it into a regular space insert.
5939
- if (newSel && change.insert.length == 2)
5940
- newSel = EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
5941
- change = { from: sel.from, to: sel.to, insert: Text.of([" "]) };
5942
- }
5943
- if (change) {
5944
- let startState = view.state;
5945
- if (browser.ios && view.inputState.flushIOSKey(view))
5946
- return true;
5947
- // Android browsers don't fire reasonable key events for enter,
5948
- // backspace, or delete. So this detects changes that look like
5949
- // they're caused by those keys, and reinterprets them as key
5950
- // events. (Some of these keys are also handled by beforeinput
5951
- // events and the pendingAndroidKey mechanism, but that's not
5952
- // reliable in all situations.)
5953
- if (browser.android &&
5954
- ((change.from == sel.from && change.to == sel.to &&
5955
- change.insert.length == 1 && change.insert.lines == 2 &&
5956
- dispatchKey(view.contentDOM, "Enter", 13)) ||
5957
- (change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 &&
5958
- dispatchKey(view.contentDOM, "Backspace", 8)) ||
5959
- (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
5960
- dispatchKey(view.contentDOM, "Delete", 46))))
5961
- return true;
5962
- let text = change.insert.toString();
5963
- if (view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text)))
5964
- return true;
5965
- if (view.inputState.composing >= 0)
5966
- view.inputState.composing++;
5967
- let tr;
5968
- if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
5969
- (!newSel || newSel.main.empty && newSel.main.from == change.from + change.insert.length) &&
5970
- view.inputState.composing < 0) {
5971
- let before = sel.from < change.from ? startState.sliceDoc(sel.from, change.from) : "";
5972
- let after = sel.to > change.to ? startState.sliceDoc(change.to, sel.to) : "";
5973
- tr = startState.replaceSelection(view.state.toText(before + change.insert.sliceString(0, undefined, view.state.lineBreak) + after));
5974
- }
5975
- else {
5976
- let changes = startState.changes(change);
5977
- let mainSel = newSel && !startState.selection.main.eq(newSel.main) && newSel.main.to <= changes.newLength
5978
- ? newSel.main : undefined;
5979
- // Try to apply a composition change to all cursors
5980
- if (startState.selection.ranges.length > 1 && view.inputState.composing >= 0 &&
5981
- change.to <= sel.to && change.to >= sel.to - 10) {
5982
- let replaced = view.state.sliceDoc(change.from, change.to);
5983
- let compositionRange = compositionSurroundingNode(view) || view.state.doc.lineAt(sel.head);
5984
- let offset = sel.to - change.to, size = sel.to - sel.from;
5985
- tr = startState.changeByRange(range => {
5986
- if (range.from == sel.from && range.to == sel.to)
5987
- return { changes, range: mainSel || range.map(changes) };
5988
- let to = range.to - offset, from = to - replaced.length;
5989
- if (range.to - range.from != size || view.state.sliceDoc(from, to) != replaced ||
5990
- // Unfortunately, there's no way to make multiple
5991
- // changes in the same node work without aborting
5992
- // composition, so cursors in the composition range are
5993
- // ignored.
5994
- compositionRange && range.to >= compositionRange.from && range.from <= compositionRange.to)
5995
- return { range };
5996
- let rangeChanges = startState.changes({ from, to, insert: change.insert }), selOff = range.to - sel.to;
5997
- return {
5998
- changes: rangeChanges,
5999
- range: !mainSel ? range.map(rangeChanges) :
6000
- EditorSelection.range(Math.max(0, mainSel.anchor + selOff), Math.max(0, mainSel.head + selOff))
6001
- };
6002
- });
6003
- }
6004
- else {
6005
- tr = {
6006
- changes,
6007
- selection: mainSel && startState.selection.replaceRange(mainSel)
6008
- };
6009
- }
6010
- }
6011
- let userEvent = "input.type";
6012
- if (view.composing) {
6013
- userEvent += ".compose";
6014
- if (view.inputState.compositionFirstChange) {
6015
- userEvent += ".start";
6016
- view.inputState.compositionFirstChange = false;
6017
- }
6018
- }
6019
- view.dispatch(tr, { scrollIntoView: true, userEvent });
6020
- return true;
6021
- }
6022
- else if (newSel && !newSel.main.eq(sel)) {
6023
- let scrollIntoView = false, userEvent = "select";
6024
- if (view.inputState.lastSelectionTime > Date.now() - 50) {
6025
- if (view.inputState.lastSelectionOrigin == "select")
6026
- scrollIntoView = true;
6027
- userEvent = view.inputState.lastSelectionOrigin;
6028
- }
6029
- view.dispatch({ selection: newSel, scrollIntoView, userEvent });
6030
- return true;
6031
- }
6032
- else {
6033
- return false;
6034
- }
6035
- }
6036
- function findDiff(a, b, preferredPos, preferredSide) {
6037
- let minLen = Math.min(a.length, b.length);
6038
- let from = 0;
6039
- while (from < minLen && a.charCodeAt(from) == b.charCodeAt(from))
6040
- from++;
6041
- if (from == minLen && a.length == b.length)
6042
- return null;
6043
- let toA = a.length, toB = b.length;
6044
- while (toA > 0 && toB > 0 && a.charCodeAt(toA - 1) == b.charCodeAt(toB - 1)) {
6045
- toA--;
6046
- toB--;
6047
- }
6048
- if (preferredSide == "end") {
6049
- let adjust = Math.max(0, from - Math.min(toA, toB));
6050
- preferredPos -= toA + adjust - from;
6051
- }
6052
- if (toA < from && a.length < b.length) {
6053
- let move = preferredPos <= from && preferredPos >= toA ? from - preferredPos : 0;
6054
- from -= move;
6055
- toB = from + (toB - toA);
6056
- toA = from;
6057
- }
6058
- else if (toB < from) {
6059
- let move = preferredPos <= from && preferredPos >= toB ? from - preferredPos : 0;
6060
- from -= move;
6061
- toA = from + (toA - toB);
6062
- toB = from;
6063
- }
6064
- return { from, toA, toB };
6065
- }
6066
- function selectionPoints(view) {
6067
- let result = [];
6068
- if (view.root.activeElement != view.contentDOM)
6069
- return result;
6070
- let { anchorNode, anchorOffset, focusNode, focusOffset } = view.observer.selectionRange;
6071
- if (anchorNode) {
6072
- result.push(new DOMPoint(anchorNode, anchorOffset));
6073
- if (focusNode != anchorNode || focusOffset != anchorOffset)
6074
- result.push(new DOMPoint(focusNode, focusOffset));
6075
- }
6076
- return result;
6077
- }
6078
- function selectionFromPoints(points, base) {
6079
- if (points.length == 0)
6080
- return null;
6081
- let anchor = points[0].pos, head = points.length == 2 ? points[1].pos : anchor;
6082
- return anchor > -1 && head > -1 ? EditorSelection.single(anchor + base, head + base) : null;
6083
- }
6084
-
6085
6155
  // The editor's update state machine looks something like this:
6086
6156
  //
6087
6157
  // Idle → Updating ⇆ Idle (unchecked) → Measuring → Idle
@@ -6144,13 +6214,7 @@ class EditorView {
6144
6214
  this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec));
6145
6215
  for (let plugin of this.plugins)
6146
6216
  plugin.update(this);
6147
- this.observer = new DOMObserver(this, (from, to, typeOver) => {
6148
- return applyDOMChange(this, from, to, typeOver);
6149
- }, event => {
6150
- this.inputState.runScrollHandlers(this, event);
6151
- if (this.observer.intersecting)
6152
- this.measure();
6153
- });
6217
+ this.observer = new DOMObserver(this);
6154
6218
  this.inputState = new InputState(this);
6155
6219
  this.inputState.ensureHandlers(this, this.plugins);
6156
6220
  this.docView = new DocView(this);
@@ -6234,7 +6298,20 @@ class EditorView {
6234
6298
  this.viewState.state = state;
6235
6299
  return;
6236
6300
  }
6237
- this.observer.clear();
6301
+ // If there was a pending DOM change, eagerly read it and try to
6302
+ // apply it after the given transactions.
6303
+ let pendingKey = this.observer.delayedAndroidKey, domChange = null;
6304
+ if (pendingKey) {
6305
+ this.observer.clearDelayedAndroidKey();
6306
+ domChange = this.observer.readChange();
6307
+ // Only try to apply DOM changes if the transactions didn't
6308
+ // change the doc or selection.
6309
+ if (domChange && !this.state.doc.eq(state.doc) || !this.state.selection.eq(state.selection))
6310
+ domChange = null;
6311
+ }
6312
+ else {
6313
+ this.observer.clear();
6314
+ }
6238
6315
  // When the phrases change, redraw the editor
6239
6316
  if (state.facet(EditorState.phrases) != this.state.facet(EditorState.phrases))
6240
6317
  return this.setState(state);
@@ -6276,6 +6353,10 @@ class EditorView {
6276
6353
  if (!update.empty)
6277
6354
  for (let listener of this.state.facet(updateListener))
6278
6355
  listener(update);
6356
+ if (domChange) {
6357
+ if (!applyDOMChange(this, domChange) && pendingKey.force)
6358
+ dispatchKey(this.contentDOM, pendingKey.key, pendingKey.keyCode);
6359
+ }
6279
6360
  }
6280
6361
  /**
6281
6362
  Reset the view to the given state. (This will cause the entire