@codemirror/view 0.19.10 → 0.19.14

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,35 @@
1
+ ## 0.19.14 (2021-11-07)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix an issue where typing into a read-only editor would move the selection.
6
+
7
+ Fix slowness when backspace is held down on iOS.
8
+
9
+ ## 0.19.13 (2021-11-06)
10
+
11
+ ### Bug fixes
12
+
13
+ Fix a bug where backspace, enter, and delete would get applied twice on iOS.
14
+
15
+ ## 0.19.12 (2021-11-04)
16
+
17
+ ### Bug fixes
18
+
19
+ Make sure the workaround for the lost virtual keyboard on Chrome Android also works on slower phones. Slight style change in beforeinput handler
20
+
21
+ Avoid failure cases in viewport-based rendering of very long lines.
22
+
23
+ ## 0.19.11 (2021-11-03)
24
+
25
+ ### Breaking changes
26
+
27
+ `EditorView.scrollPosIntoView` has been deprecated. Use the `EditorView.scrollTo` effect instead.
28
+
29
+ ### New features
30
+
31
+ The new `EditorView.centerOn` effect can be used to scroll a given range to the center of the view.
32
+
1
33
  ## 0.19.10 (2021-11-02)
2
34
 
3
35
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -102,9 +102,9 @@ function windowRect(win) {
102
102
  top: 0, bottom: win.innerHeight };
103
103
  }
104
104
  const ScrollSpace = 5;
105
- function scrollRectIntoView(dom, rect, side) {
105
+ function scrollRectIntoView(dom, rect, side, center) {
106
106
  let doc = dom.ownerDocument, win = doc.defaultView;
107
- for (let cur = dom.parentNode; cur;) {
107
+ for (let cur = dom; cur;) {
108
108
  if (cur.nodeType == 1) { // Element
109
109
  let bounding, top = cur == doc.body;
110
110
  if (top) {
@@ -121,7 +121,20 @@ function scrollRectIntoView(dom, rect, side) {
121
121
  top: rect.top, bottom: rect.top + cur.clientHeight };
122
122
  }
123
123
  let moveX = 0, moveY = 0;
124
- if (rect.top < bounding.top) {
124
+ if (center) {
125
+ let rectHeight = rect.bottom - rect.top, boundingHeight = bounding.bottom - bounding.top;
126
+ let targetTop;
127
+ if (rectHeight <= boundingHeight)
128
+ targetTop = rect.top + rectHeight / 2 - boundingHeight / 2;
129
+ else if (side < 0)
130
+ targetTop = rect.top - ScrollSpace;
131
+ else
132
+ targetTop = rect.bottom + ScrollSpace - boundingHeight;
133
+ moveY = targetTop - bounding.top;
134
+ if (Math.abs(moveY) <= 1)
135
+ moveY = 0;
136
+ }
137
+ else if (rect.top < bounding.top) {
125
138
  moveY = -(bounding.top - rect.top + ScrollSpace);
126
139
  if (side > 0 && rect.bottom > bounding.bottom + moveY)
127
140
  moveY = rect.bottom - bounding.bottom + moveY + ScrollSpace;
@@ -163,6 +176,7 @@ function scrollRectIntoView(dom, rect, side) {
163
176
  if (top)
164
177
  break;
165
178
  cur = cur.assignedSlot || cur.parentNode;
179
+ center = false;
166
180
  }
167
181
  else if (cur.nodeType == 11) { // A shadow root
168
182
  cur = cur.host;
@@ -253,9 +267,9 @@ function contentEditablePlainTextSupported() {
253
267
  }
254
268
  function getRoot(node) {
255
269
  while (node) {
256
- node = node.assignedSlot || node.parentNode;
257
270
  if (node && (node.nodeType == 9 || node.nodeType == 11 && node.host))
258
271
  return node;
272
+ node = node.assignedSlot || node.parentNode;
259
273
  }
260
274
  return null;
261
275
  }
@@ -307,25 +321,30 @@ class ContentView {
307
321
  sync(track) {
308
322
  var _a;
309
323
  if (this.dirty & 2 /* Node */) {
310
- let parent = this.dom, pos = null;
324
+ let parent = this.dom;
325
+ let pos = parent.firstChild;
311
326
  for (let child of this.children) {
312
327
  if (child.dirty) {
313
- let next = pos ? pos.nextSibling : parent.firstChild;
314
- if (!child.dom && next && !((_a = ContentView.get(next)) === null || _a === void 0 ? void 0 : _a.parent))
315
- child.reuseDOM(next);
328
+ if (!child.dom && pos && !((_a = ContentView.get(pos)) === null || _a === void 0 ? void 0 : _a.parent))
329
+ child.reuseDOM(pos);
316
330
  child.sync(track);
317
331
  child.dirty = 0 /* Not */;
318
332
  }
319
- if (track && track.node == parent && pos != child.dom)
333
+ if (track && !track.written && track.node == parent && pos != child.dom)
320
334
  track.written = true;
321
- syncNodeInto(parent, pos, child.dom);
322
- pos = child.dom;
335
+ if (child.dom.parentNode == parent) {
336
+ while (pos && pos != child.dom)
337
+ pos = rm(pos);
338
+ pos = child.dom.nextSibling;
339
+ }
340
+ else {
341
+ parent.insertBefore(child.dom, pos);
342
+ }
323
343
  }
324
- let next = pos ? pos.nextSibling : parent.firstChild;
325
- if (next && track && track.node == parent)
344
+ if (pos && track && track.node == parent)
326
345
  track.written = true;
327
- while (next)
328
- next = rm(next);
346
+ while (pos)
347
+ pos = rm(pos);
329
348
  }
330
349
  else if (this.dirty & 1 /* Child */) {
331
350
  for (let child of this.children)
@@ -465,14 +484,6 @@ function rm(dom) {
465
484
  dom.parentNode.removeChild(dom);
466
485
  return next;
467
486
  }
468
- function syncNodeInto(parent, after, dom) {
469
- let next = after ? after.nextSibling : parent.firstChild;
470
- if (dom.parentNode == parent)
471
- while (next != dom)
472
- next = rm(next);
473
- else
474
- parent.insertBefore(dom, next);
475
- }
476
487
  class ChildCursor {
477
488
  constructor(children, pos, i) {
478
489
  this.children = children;
@@ -1560,6 +1571,9 @@ const inputHandler = state.Facet.define();
1560
1571
  const scrollTo = state.StateEffect.define({
1561
1572
  map: (range, changes) => range.map(changes)
1562
1573
  });
1574
+ const centerOn = state.StateEffect.define({
1575
+ map: (range, changes) => range.map(changes)
1576
+ });
1563
1577
  /**
1564
1578
  Log or report an unhandled exception in client code. Should
1565
1579
  probably only be used by extension code that allows client code to
@@ -2304,7 +2318,7 @@ class DocView extends ContentView {
2304
2318
  this.view.viewState.lineGapDeco
2305
2319
  ];
2306
2320
  }
2307
- scrollRangeIntoView(range) {
2321
+ scrollIntoView({ range, center }) {
2308
2322
  let rect = this.coordsAt(range.head, range.empty ? range.assoc : range.head > range.anchor ? -1 : 1), other;
2309
2323
  if (!rect)
2310
2324
  return;
@@ -2324,10 +2338,10 @@ class DocView extends ContentView {
2324
2338
  if (bottom != null)
2325
2339
  mBottom = Math.max(mBottom, bottom);
2326
2340
  }
2327
- scrollRectIntoView(this.dom, {
2341
+ scrollRectIntoView(this.view.scrollDOM, {
2328
2342
  left: rect.left - mLeft, top: rect.top - mTop,
2329
2343
  right: rect.right + mRight, bottom: rect.bottom + mBottom
2330
- }, range.head < range.anchor ? -1 : 1);
2344
+ }, range.head < range.anchor ? -1 : 1, center);
2331
2345
  }
2332
2346
  }
2333
2347
  function betweenUneditable(pos) {
@@ -3064,10 +3078,6 @@ class InputState {
3064
3078
  constructor(view) {
3065
3079
  this.lastKeyCode = 0;
3066
3080
  this.lastKeyTime = 0;
3067
- // On iOS, some keys need to have their default behavior happen
3068
- // (after which we retroactively handle them and reset the DOM) to
3069
- // avoid messing up the virtual keyboard state.
3070
- //
3071
3081
  // On Chrome Android, backspace near widgets is just completely
3072
3082
  // broken, and there are no key events, so we need to handle the
3073
3083
  // beforeinput event. Deleting stuff will often create a flurry of
@@ -3075,12 +3085,11 @@ class InputState {
3075
3085
  // subsequent events even more broken, so again, we hold off doing
3076
3086
  // anything until the browser is finished with whatever it is trying
3077
3087
  // to do.
3078
- //
3079
- // setPendingKey sets this, causing the DOM observer to pause for a
3080
- // bit, and setting an animation frame (which seems the most
3081
- // reliable way to detect 'browser is done flailing') to fire a fake
3082
- // key event and re-sync the view again.
3083
- this.pendingKey = undefined;
3088
+ this.pendingAndroidKey = undefined;
3089
+ // On iOS, some keys need to have their default behavior happen
3090
+ // (after which we retroactively handle them and reset the DOM) to
3091
+ // avoid messing up the virtual keyboard state.
3092
+ this.pendingIOSKey = undefined;
3084
3093
  this.lastSelectionOrigin = null;
3085
3094
  this.lastSelectionTime = 0;
3086
3095
  this.lastEscPress = 0;
@@ -3192,18 +3201,30 @@ class InputState {
3192
3201
  let pending;
3193
3202
  if (browser.ios && (pending = PendingKeys.find(key => key.keyCode == event.keyCode)) &&
3194
3203
  !(event.ctrlKey || event.altKey || event.metaKey) && !event.synthetic) {
3195
- this.setPendingKey(view, pending);
3204
+ this.pendingIOSKey = pending;
3205
+ setTimeout(() => this.flushIOSKey(view), 250);
3196
3206
  return true;
3197
3207
  }
3198
3208
  return false;
3199
3209
  }
3200
- setPendingKey(view, pending) {
3201
- this.pendingKey = pending;
3210
+ flushIOSKey(view) {
3211
+ let key = this.pendingIOSKey;
3212
+ if (!key)
3213
+ return false;
3214
+ this.pendingIOSKey = undefined;
3215
+ return dispatchKey(view.contentDOM, key.key, key.keyCode);
3216
+ }
3217
+ // This causes the DOM observer to pause for a bit, and sets an
3218
+ // animation frame (which seems the most reliable way to detect
3219
+ // 'Chrome is done flailing about messing with the DOM') to fire a
3220
+ // fake key event and re-sync the view again.
3221
+ setPendingAndroidKey(view, pending) {
3222
+ this.pendingAndroidKey = pending;
3202
3223
  requestAnimationFrame(() => {
3203
- if (!this.pendingKey)
3204
- return false;
3205
- let key = this.pendingKey;
3206
- this.pendingKey = undefined;
3224
+ let key = this.pendingAndroidKey;
3225
+ if (!key)
3226
+ return;
3227
+ this.pendingAndroidKey = undefined;
3207
3228
  view.observer.processRecords();
3208
3229
  let startState = view.state;
3209
3230
  dispatchKey(view.contentDOM, key.key, key.keyCode);
@@ -3717,18 +3738,20 @@ handlers.beforeinput = (view, event) => {
3717
3738
  // seems to do nothing at all on Chrome).
3718
3739
  let pending;
3719
3740
  if (browser.chrome && browser.android && (pending = PendingKeys.find(key => key.inputType == event.inputType))) {
3720
- view.inputState.setPendingKey(view, pending);
3721
- let startViewHeight = ((_a = window.visualViewport) === null || _a === void 0 ? void 0 : _a.height) || 0;
3722
- setTimeout(() => {
3723
- var _a;
3724
- // Backspacing near uneditable nodes on Chrome Android sometimes
3725
- // closes the virtual keyboard. This tries to crudely detect
3726
- // that and refocus to get it back.
3727
- if ((((_a = window.visualViewport) === null || _a === void 0 ? void 0 : _a.height) || 0) > startViewHeight + 10 && view.hasFocus) {
3728
- view.contentDOM.blur();
3729
- view.focus();
3730
- }
3731
- }, 50);
3741
+ view.inputState.setPendingAndroidKey(view, pending);
3742
+ if (pending.key == "Backspace" || pending.key == "Delete") {
3743
+ let startViewHeight = ((_a = window.visualViewport) === null || _a === void 0 ? void 0 : _a.height) || 0;
3744
+ setTimeout(() => {
3745
+ var _a;
3746
+ // Backspacing near uneditable nodes on Chrome Android sometimes
3747
+ // closes the virtual keyboard. This tries to crudely detect
3748
+ // that and refocus to get it back.
3749
+ if ((((_a = window.visualViewport) === null || _a === void 0 ? void 0 : _a.height) || 0) > startViewHeight + 10 && view.hasFocus) {
3750
+ view.contentDOM.blur();
3751
+ view.focus();
3752
+ }
3753
+ }, 100);
3754
+ }
3732
3755
  }
3733
3756
  };
3734
3757
 
@@ -4446,6 +4469,15 @@ class LineGapWidget extends WidgetType {
4446
4469
  }
4447
4470
  get estimatedHeight() { return this.vertical ? this.size : -1; }
4448
4471
  }
4472
+ class ScrollTarget {
4473
+ constructor(range, center = false) {
4474
+ this.range = range;
4475
+ this.center = center;
4476
+ }
4477
+ map(changes) {
4478
+ return changes.empty ? this : new ScrollTarget(this.range.map(changes), this.center);
4479
+ }
4480
+ }
4449
4481
  class ViewState {
4450
4482
  constructor(state) {
4451
4483
  this.state = state;
@@ -4458,7 +4490,7 @@ class ViewState {
4458
4490
  this.heightOracle = new HeightOracle;
4459
4491
  // See VP.MaxDOMHeight
4460
4492
  this.scaler = IdScaler;
4461
- this.scrollTo = null;
4493
+ this.scrollTarget = null;
4462
4494
  // Briefly set to true when printing, to disable viewport limiting
4463
4495
  this.printing = false;
4464
4496
  this.visibleRanges = [];
@@ -4491,7 +4523,7 @@ class ViewState {
4491
4523
  this.scaler = this.heightMap.height <= 7000000 /* MaxDOMHeight */ ? IdScaler :
4492
4524
  new BigScaler(this.heightOracle.doc, this.heightMap, this.viewports);
4493
4525
  }
4494
- update(update, scrollTo = null) {
4526
+ update(update, scrollTarget = null) {
4495
4527
  let prev = this.state;
4496
4528
  this.state = update.state;
4497
4529
  let newDeco = this.state.facet(decorations);
@@ -4502,15 +4534,16 @@ class ViewState {
4502
4534
  if (this.heightMap.height != prevHeight)
4503
4535
  update.flags |= 2 /* Height */;
4504
4536
  let viewport = heightChanges.length ? this.mapViewport(this.viewport, update.changes) : this.viewport;
4505
- if (scrollTo && (scrollTo.head < viewport.from || scrollTo.head > viewport.to) || !this.viewportIsAppropriate(viewport))
4506
- viewport = this.getViewport(0, scrollTo);
4537
+ if (scrollTarget && (scrollTarget.range.head < viewport.from || scrollTarget.range.head > viewport.to) ||
4538
+ !this.viewportIsAppropriate(viewport))
4539
+ viewport = this.getViewport(0, scrollTarget);
4507
4540
  this.viewport = viewport;
4508
4541
  this.updateForViewport();
4509
- if (this.lineGaps.length || this.viewport.to - this.viewport.from > 15000 /* MinViewPort */)
4542
+ if (this.lineGaps.length || this.viewport.to - this.viewport.from > 4000 /* DoubleMargin */)
4510
4543
  this.updateLineGaps(this.ensureLineGaps(this.mapLineGaps(this.lineGaps, update.changes)));
4511
4544
  update.flags |= this.computeVisibleRanges();
4512
- if (scrollTo)
4513
- this.scrollTo = scrollTo;
4545
+ if (scrollTarget)
4546
+ this.scrollTarget = scrollTarget;
4514
4547
  if (!this.mustEnforceCursorAssoc && update.selectionSet && update.view.lineWrapping &&
4515
4548
  update.state.selection.main.empty && update.state.selection.main.assoc)
4516
4549
  this.mustEnforceCursorAssoc = true;
@@ -4563,10 +4596,10 @@ class ViewState {
4563
4596
  if (oracle.heightChanged)
4564
4597
  result |= 2 /* Height */;
4565
4598
  if (!this.viewportIsAppropriate(this.viewport, bias) ||
4566
- this.scrollTo && (this.scrollTo.head < this.viewport.from || this.scrollTo.head > this.viewport.to))
4567
- this.viewport = this.getViewport(bias, this.scrollTo);
4599
+ this.scrollTarget && (this.scrollTarget.range.head < this.viewport.from || this.scrollTarget.range.head > this.viewport.to))
4600
+ this.viewport = this.getViewport(bias, this.scrollTarget);
4568
4601
  this.updateForViewport();
4569
- if (this.lineGaps.length || this.viewport.to - this.viewport.from > 15000 /* MinViewPort */)
4602
+ if (this.lineGaps.length || this.viewport.to - this.viewport.from > 4000 /* DoubleMargin */)
4570
4603
  this.updateLineGaps(this.ensureLineGaps(refresh ? [] : this.lineGaps));
4571
4604
  result |= this.computeVisibleRanges();
4572
4605
  if (this.mustEnforceCursorAssoc) {
@@ -4581,22 +4614,25 @@ class ViewState {
4581
4614
  }
4582
4615
  get visibleTop() { return this.scaler.fromDOM(this.pixelViewport.top, 0); }
4583
4616
  get visibleBottom() { return this.scaler.fromDOM(this.pixelViewport.bottom, 0); }
4584
- getViewport(bias, scrollTo) {
4617
+ getViewport(bias, scrollTarget) {
4585
4618
  // This will divide VP.Margin between the top and the
4586
4619
  // bottom, depending on the bias (the change in viewport position
4587
4620
  // since the last update). It'll hold a number between 0 and 1
4588
4621
  let marginTop = 0.5 - Math.max(-0.5, Math.min(0.5, bias / 1000 /* Margin */ / 2));
4589
4622
  let map = this.heightMap, doc = this.state.doc, { visibleTop, visibleBottom } = this;
4590
4623
  let viewport = new Viewport(map.lineAt(visibleTop - marginTop * 1000 /* Margin */, QueryType.ByHeight, doc, 0, 0).from, map.lineAt(visibleBottom + (1 - marginTop) * 1000 /* Margin */, QueryType.ByHeight, doc, 0, 0).to);
4591
- // If scrollTo is given, make sure the viewport includes that position
4592
- if (scrollTo) {
4593
- if (scrollTo.head < viewport.from) {
4594
- let { top: newTop } = map.lineAt(scrollTo.head, QueryType.ByPos, doc, 0, 0);
4595
- viewport = new Viewport(map.lineAt(newTop - 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).from, map.lineAt(newTop + (visibleBottom - visibleTop) + 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).to);
4596
- }
4597
- else if (scrollTo.head > viewport.to) {
4598
- let { bottom: newBottom } = map.lineAt(scrollTo.head, QueryType.ByPos, doc, 0, 0);
4599
- viewport = new Viewport(map.lineAt(newBottom - (visibleBottom - visibleTop) - 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).from, map.lineAt(newBottom + 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).to);
4624
+ // If scrollTarget is given, make sure the viewport includes that position
4625
+ if (scrollTarget) {
4626
+ let { head } = scrollTarget.range, viewHeight = visibleBottom - visibleTop;
4627
+ if (head < viewport.from || head > viewport.to) {
4628
+ let block = map.lineAt(head, QueryType.ByPos, doc, 0, 0), topPos;
4629
+ if (scrollTarget.center)
4630
+ topPos = (block.top + block.bottom) / 2 - viewHeight / 2;
4631
+ else if (head < viewport.from)
4632
+ topPos = block.top;
4633
+ else
4634
+ topPos = block.bottom - viewHeight;
4635
+ viewport = new Viewport(map.lineAt(topPos - 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).from, map.lineAt(topPos + viewHeight + 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).to);
4600
4636
  }
4601
4637
  }
4602
4638
  return viewport;
@@ -4638,52 +4674,50 @@ class ViewState {
4638
4674
  if (this.heightOracle.direction != exports.Direction.LTR)
4639
4675
  return gaps;
4640
4676
  this.heightMap.forEachLine(this.viewport.from, this.viewport.to, this.state.doc, 0, 0, line => {
4641
- if (line.length < 10000 /* Margin */)
4677
+ if (line.length < 4000 /* DoubleMargin */)
4642
4678
  return;
4643
4679
  let structure = lineStructure(line.from, line.to, this.state);
4644
- if (structure.total < 10000 /* Margin */)
4680
+ if (structure.total < 4000 /* DoubleMargin */)
4645
4681
  return;
4646
4682
  let viewFrom, viewTo;
4647
4683
  if (this.heightOracle.lineWrapping) {
4648
- if (line.from != this.viewport.from)
4649
- viewFrom = line.from;
4650
- else
4651
- viewFrom = findPosition(structure, (this.visibleTop - line.top) / line.height);
4652
- if (line.to != this.viewport.to)
4653
- viewTo = line.to;
4654
- else
4655
- viewTo = findPosition(structure, (this.visibleBottom - line.top) / line.height);
4684
+ let marginHeight = (2000 /* Margin */ / this.heightOracle.lineLength) * this.heightOracle.lineHeight;
4685
+ viewFrom = findPosition(structure, (this.visibleTop - line.top - marginHeight) / line.height);
4686
+ viewTo = findPosition(structure, (this.visibleBottom - line.top + marginHeight) / line.height);
4656
4687
  }
4657
4688
  else {
4658
4689
  let totalWidth = structure.total * this.heightOracle.charWidth;
4659
- viewFrom = findPosition(structure, this.pixelViewport.left / totalWidth);
4660
- viewTo = findPosition(structure, this.pixelViewport.right / totalWidth);
4690
+ let marginWidth = 2000 /* Margin */ * this.heightOracle.charWidth;
4691
+ viewFrom = findPosition(structure, (this.pixelViewport.left - marginWidth) / totalWidth);
4692
+ viewTo = findPosition(structure, (this.pixelViewport.right + marginWidth) / totalWidth);
4661
4693
  }
4694
+ let outside = [];
4695
+ if (viewFrom > line.from)
4696
+ outside.push({ from: line.from, to: viewFrom });
4697
+ if (viewTo < line.to)
4698
+ outside.push({ from: viewTo, to: line.to });
4662
4699
  let sel = this.state.selection.main;
4663
- // Make sure the gap doesn't cover a selection end
4664
- if (sel.from <= viewFrom && sel.to >= line.from)
4665
- viewFrom = sel.from;
4666
- if (sel.from <= line.to && sel.to >= viewTo)
4667
- viewTo = sel.to;
4668
- let gapTo = viewFrom - 10000 /* Margin */, gapFrom = viewTo + 10000 /* Margin */;
4669
- if (gapTo > line.from + 5000 /* HalfMargin */)
4670
- gaps.push(find(current, gap => gap.from == line.from && gap.to > gapTo - 5000 /* HalfMargin */ && gap.to < gapTo + 5000 /* HalfMargin */) ||
4671
- new LineGap(line.from, gapTo, this.gapSize(line, gapTo, true, structure)));
4672
- if (gapFrom < line.to - 5000 /* HalfMargin */)
4673
- gaps.push(find(current, gap => gap.to == line.to && gap.from > gapFrom - 5000 /* HalfMargin */ &&
4674
- gap.from < gapFrom + 5000 /* HalfMargin */) ||
4675
- new LineGap(gapFrom, line.to, this.gapSize(line, gapFrom, false, structure)));
4700
+ // Make sure the gaps don't cover a selection end
4701
+ if (sel.from >= line.from && sel.from <= line.to)
4702
+ cutRange(outside, sel.from - 10 /* SelectionMargin */, sel.from + 10 /* SelectionMargin */);
4703
+ if (!sel.empty && sel.to >= line.from && sel.to <= line.to)
4704
+ cutRange(outside, sel.to - 10 /* SelectionMargin */, sel.to + 10 /* SelectionMargin */);
4705
+ for (let { from, to } of outside)
4706
+ if (to - from > 1000 /* HalfMargin */) {
4707
+ gaps.push(find(current, gap => gap.from >= line.from && gap.to <= line.to &&
4708
+ Math.abs(gap.from - from) < 1000 /* HalfMargin */ && Math.abs(gap.to - to) < 1000 /* HalfMargin */) ||
4709
+ new LineGap(from, to, this.gapSize(line, from, to, structure)));
4710
+ }
4676
4711
  });
4677
4712
  return gaps;
4678
4713
  }
4679
- gapSize(line, pos, start, structure) {
4714
+ gapSize(line, from, to, structure) {
4715
+ let fraction = findFraction(structure, to) - findFraction(structure, from);
4680
4716
  if (this.heightOracle.lineWrapping) {
4681
- let height = line.height * findFraction(structure, pos);
4682
- return start ? height : line.height - height;
4717
+ return line.height * fraction;
4683
4718
  }
4684
4719
  else {
4685
- let ratio = findFraction(structure, pos);
4686
- return structure.total * this.heightOracle.charWidth * (start ? ratio : 1 - ratio);
4720
+ return structure.total * this.heightOracle.charWidth * fraction;
4687
4721
  }
4688
4722
  }
4689
4723
  updateLineGaps(gaps) {
@@ -4777,6 +4811,20 @@ function findFraction(structure, pos) {
4777
4811
  }
4778
4812
  return counted / structure.total;
4779
4813
  }
4814
+ function cutRange(ranges, from, to) {
4815
+ for (let i = 0; i < ranges.length; i++) {
4816
+ let r = ranges[i];
4817
+ if (r.from < to && r.to > from) {
4818
+ let pieces = [];
4819
+ if (r.from < from)
4820
+ pieces.push({ from: r.from, to: from });
4821
+ if (r.to > to)
4822
+ pieces.push({ from: to, to: r.to });
4823
+ ranges.splice(i, 1, ...pieces);
4824
+ i += pieces.length - 1;
4825
+ }
4826
+ }
4827
+ }
4780
4828
  function find(array, f) {
4781
4829
  for (let val of array)
4782
4830
  if (f(val))
@@ -5238,7 +5286,7 @@ class DOMObserver {
5238
5286
  // Completely hold off flushing when pending keys are set—the code
5239
5287
  // managing those will make sure processRecords is called and the
5240
5288
  // view is resynchronized after
5241
- if (this.delayedFlush >= 0 || this.view.inputState.pendingKey)
5289
+ if (this.delayedFlush >= 0 || this.view.inputState.pendingAndroidKey)
5242
5290
  return;
5243
5291
  this.lastFlush = Date.now();
5244
5292
  let { from, to, typeOver } = this.processRecords();
@@ -5325,8 +5373,11 @@ function safariSelectionRangeHack(view) {
5325
5373
 
5326
5374
  function applyDOMChange(view, start, end, typeOver) {
5327
5375
  let change, newSel;
5328
- let sel = view.state.selection.main, bounds;
5329
- if (start > -1 && !view.state.readOnly && (bounds = view.docView.domBoundsAround(start, end, 0))) {
5376
+ let sel = view.state.selection.main;
5377
+ if (start > -1) {
5378
+ let bounds = view.docView.domBoundsAround(start, end, 0);
5379
+ if (!bounds || view.state.readOnly)
5380
+ return;
5330
5381
  let { from, to } = bounds;
5331
5382
  let selPoints = view.docView.impreciseHead || view.docView.impreciseAnchor ? [] : selectionPoints(view);
5332
5383
  let reader = new DOMReader(selPoints, view);
@@ -5380,16 +5431,8 @@ function applyDOMChange(view, start, end, typeOver) {
5380
5431
  // backspace, or delete. So this detects changes that look like
5381
5432
  // they're caused by those keys, and reinterprets them as key
5382
5433
  // events.
5383
- if (browser.android &&
5384
- ((change.from == sel.from && change.to == sel.to &&
5385
- change.insert.length == 1 && change.insert.lines == 2 &&
5386
- dispatchKey(view.contentDOM, "Enter", 13)) ||
5387
- (change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 &&
5388
- dispatchKey(view.contentDOM, "Backspace", 8)) ||
5389
- (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
5390
- dispatchKey(view.contentDOM, "Delete", 46)))) {
5434
+ if (browser.ios && view.inputState.flushIOSKey(view))
5391
5435
  return;
5392
- }
5393
5436
  let text = change.insert.toString();
5394
5437
  if (view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text)))
5395
5438
  return;
@@ -5691,21 +5734,24 @@ class EditorView {
5691
5734
  if (state$1.facet(state.EditorState.phrases) != this.state.facet(state.EditorState.phrases))
5692
5735
  return this.setState(state$1);
5693
5736
  update = new ViewUpdate(this, state$1, transactions);
5694
- let scrollPos = null;
5737
+ let scrollTarget = null;
5695
5738
  try {
5696
5739
  this.updateState = 2 /* Updating */;
5697
5740
  for (let tr of transactions) {
5698
- if (scrollPos)
5699
- scrollPos = scrollPos.map(tr.changes);
5741
+ if (scrollTarget)
5742
+ scrollTarget = scrollTarget.map(tr.changes);
5700
5743
  if (tr.scrollIntoView) {
5701
5744
  let { main } = tr.state.selection;
5702
- scrollPos = main.empty ? main : state.EditorSelection.cursor(main.head, main.head > main.anchor ? -1 : 1);
5745
+ scrollTarget = new ScrollTarget(main.empty ? main : state.EditorSelection.cursor(main.head, main.head > main.anchor ? -1 : 1));
5703
5746
  }
5704
- for (let e of tr.effects)
5747
+ for (let e of tr.effects) {
5705
5748
  if (e.is(scrollTo))
5706
- scrollPos = e.value;
5749
+ scrollTarget = new ScrollTarget(e.value);
5750
+ else if (e.is(centerOn))
5751
+ scrollTarget = new ScrollTarget(e.value, true);
5752
+ }
5707
5753
  }
5708
- this.viewState.update(update, scrollPos);
5754
+ this.viewState.update(update, scrollTarget);
5709
5755
  this.bidiCache = CachedOrder.update(this.bidiCache, update.changes);
5710
5756
  if (!update.empty) {
5711
5757
  this.updatePlugins(update);
@@ -5720,7 +5766,7 @@ class EditorView {
5720
5766
  finally {
5721
5767
  this.updateState = 0 /* Idle */;
5722
5768
  }
5723
- if (redrawn || scrollPos || this.viewState.mustEnforceCursorAssoc)
5769
+ if (redrawn || scrollTarget || this.viewState.mustEnforceCursorAssoc)
5724
5770
  this.requestMeasure();
5725
5771
  if (!update.empty)
5726
5772
  for (let listener of this.state.facet(updateListener))
@@ -5802,7 +5848,7 @@ class EditorView {
5802
5848
  this.updateState = 1 /* Measuring */;
5803
5849
  let oldViewport = this.viewport;
5804
5850
  let changed = this.viewState.measure(this.docView, i > 0);
5805
- if (!changed && !this.measureRequests.length && this.viewState.scrollTo == null)
5851
+ if (!changed && !this.measureRequests.length && this.viewState.scrollTarget == null)
5806
5852
  break;
5807
5853
  if (i > 5) {
5808
5854
  console.warn("Viewport failed to stabilize");
@@ -5844,9 +5890,9 @@ class EditorView {
5844
5890
  logException(this.state, e);
5845
5891
  }
5846
5892
  }
5847
- if (this.viewState.scrollTo) {
5848
- this.docView.scrollRangeIntoView(this.viewState.scrollTo);
5849
- this.viewState.scrollTo = null;
5893
+ if (this.viewState.scrollTarget) {
5894
+ this.docView.scrollIntoView(this.viewState.scrollTarget);
5895
+ this.viewState.scrollTarget = null;
5850
5896
  }
5851
5897
  if (this.viewport.from == oldViewport.from && this.viewport.to == oldViewport.to && this.measureRequests.length == 0)
5852
5898
  break;
@@ -6069,12 +6115,9 @@ class EditorView {
6069
6115
  moveVertically(start, forward, distance) {
6070
6116
  return skipAtoms(this, start, moveVertically(this, start, forward, distance));
6071
6117
  }
6072
- /**
6073
- Scroll the given document position into view.
6074
- */
6118
+ // FIXME remove on next major version
6075
6119
  scrollPosIntoView(pos) {
6076
- this.viewState.scrollTo = state.EditorSelection.cursor(pos);
6077
- this.requestMeasure();
6120
+ this.dispatch({ effects: scrollTo.of(state.EditorSelection.cursor(pos)) });
6078
6121
  }
6079
6122
  /**
6080
6123
  Find the DOM parent node and offset (child offset if `node` is
@@ -6241,7 +6284,7 @@ class EditorView {
6241
6284
  target editors with a dark or light theme.
6242
6285
  */
6243
6286
  static baseTheme(spec) {
6244
- return state.Prec.fallback(styleModule.of(buildTheme("." + baseThemeID, spec, lightDarkIDs)));
6287
+ return state.Prec.lowest(styleModule.of(buildTheme("." + baseThemeID, spec, lightDarkIDs)));
6245
6288
  }
6246
6289
  }
6247
6290
  /**
@@ -6250,6 +6293,11 @@ transaction to make it scroll the given range into view.
6250
6293
  */
6251
6294
  EditorView.scrollTo = scrollTo;
6252
6295
  /**
6296
+ Effect that makes the editor scroll the given range to the
6297
+ center of the visible view.
6298
+ */
6299
+ EditorView.centerOn = centerOn;
6300
+ /**
6253
6301
  Facet to add a [style
6254
6302
  module](https://github.com/marijnh/style-mod#documentation) to
6255
6303
  an editor view. The view will ensure that the module is
@@ -6678,7 +6726,7 @@ const themeSpec = {
6678
6726
  };
6679
6727
  if (CanHidePrimary)
6680
6728
  themeSpec[".cm-line"].caretColor = "transparent !important";
6681
- const hideNativeSelection = state.Prec.override(EditorView.theme(themeSpec));
6729
+ const hideNativeSelection = state.Prec.highest(EditorView.theme(themeSpec));
6682
6730
  function getBase(view) {
6683
6731
  let rect = view.scrollDOM.getBoundingClientRect();
6684
6732
  let left = view.textDirection == exports.Direction.LTR ? rect.left : rect.right - view.scrollDOM.clientWidth;
package/dist/index.d.ts CHANGED
@@ -856,9 +856,6 @@ declare class EditorView {
856
856
  used.
857
857
  */
858
858
  moveVertically(start: SelectionRange, forward: boolean, distance?: number): SelectionRange;
859
- /**
860
- Scroll the given document position into view.
861
- */
862
859
  scrollPosIntoView(pos: number): void;
863
860
  /**
864
861
  Find the DOM parent node and offset (child offset if `node` is
@@ -949,6 +946,11 @@ declare class EditorView {
949
946
  */
950
947
  static scrollTo: _codemirror_state.StateEffectType<SelectionRange>;
951
948
  /**
949
+ Effect that makes the editor scroll the given range to the
950
+ center of the visible view.
951
+ */
952
+ static centerOn: _codemirror_state.StateEffectType<SelectionRange>;
953
+ /**
952
954
  Facet to add a [style
953
955
  module](https://github.com/marijnh/style-mod#documentation) to
954
956
  an editor view. The view will ensure that the module is
package/dist/index.js CHANGED
@@ -99,9 +99,9 @@ function windowRect(win) {
99
99
  top: 0, bottom: win.innerHeight };
100
100
  }
101
101
  const ScrollSpace = 5;
102
- function scrollRectIntoView(dom, rect, side) {
102
+ function scrollRectIntoView(dom, rect, side, center) {
103
103
  let doc = dom.ownerDocument, win = doc.defaultView;
104
- for (let cur = dom.parentNode; cur;) {
104
+ for (let cur = dom; cur;) {
105
105
  if (cur.nodeType == 1) { // Element
106
106
  let bounding, top = cur == doc.body;
107
107
  if (top) {
@@ -118,7 +118,20 @@ function scrollRectIntoView(dom, rect, side) {
118
118
  top: rect.top, bottom: rect.top + cur.clientHeight };
119
119
  }
120
120
  let moveX = 0, moveY = 0;
121
- if (rect.top < bounding.top) {
121
+ if (center) {
122
+ let rectHeight = rect.bottom - rect.top, boundingHeight = bounding.bottom - bounding.top;
123
+ let targetTop;
124
+ if (rectHeight <= boundingHeight)
125
+ targetTop = rect.top + rectHeight / 2 - boundingHeight / 2;
126
+ else if (side < 0)
127
+ targetTop = rect.top - ScrollSpace;
128
+ else
129
+ targetTop = rect.bottom + ScrollSpace - boundingHeight;
130
+ moveY = targetTop - bounding.top;
131
+ if (Math.abs(moveY) <= 1)
132
+ moveY = 0;
133
+ }
134
+ else if (rect.top < bounding.top) {
122
135
  moveY = -(bounding.top - rect.top + ScrollSpace);
123
136
  if (side > 0 && rect.bottom > bounding.bottom + moveY)
124
137
  moveY = rect.bottom - bounding.bottom + moveY + ScrollSpace;
@@ -160,6 +173,7 @@ function scrollRectIntoView(dom, rect, side) {
160
173
  if (top)
161
174
  break;
162
175
  cur = cur.assignedSlot || cur.parentNode;
176
+ center = false;
163
177
  }
164
178
  else if (cur.nodeType == 11) { // A shadow root
165
179
  cur = cur.host;
@@ -250,9 +264,9 @@ function contentEditablePlainTextSupported() {
250
264
  }
251
265
  function getRoot(node) {
252
266
  while (node) {
253
- node = node.assignedSlot || node.parentNode;
254
267
  if (node && (node.nodeType == 9 || node.nodeType == 11 && node.host))
255
268
  return node;
269
+ node = node.assignedSlot || node.parentNode;
256
270
  }
257
271
  return null;
258
272
  }
@@ -304,25 +318,30 @@ class ContentView {
304
318
  sync(track) {
305
319
  var _a;
306
320
  if (this.dirty & 2 /* Node */) {
307
- let parent = this.dom, pos = null;
321
+ let parent = this.dom;
322
+ let pos = parent.firstChild;
308
323
  for (let child of this.children) {
309
324
  if (child.dirty) {
310
- let next = pos ? pos.nextSibling : parent.firstChild;
311
- if (!child.dom && next && !((_a = ContentView.get(next)) === null || _a === void 0 ? void 0 : _a.parent))
312
- child.reuseDOM(next);
325
+ if (!child.dom && pos && !((_a = ContentView.get(pos)) === null || _a === void 0 ? void 0 : _a.parent))
326
+ child.reuseDOM(pos);
313
327
  child.sync(track);
314
328
  child.dirty = 0 /* Not */;
315
329
  }
316
- if (track && track.node == parent && pos != child.dom)
330
+ if (track && !track.written && track.node == parent && pos != child.dom)
317
331
  track.written = true;
318
- syncNodeInto(parent, pos, child.dom);
319
- pos = child.dom;
332
+ if (child.dom.parentNode == parent) {
333
+ while (pos && pos != child.dom)
334
+ pos = rm(pos);
335
+ pos = child.dom.nextSibling;
336
+ }
337
+ else {
338
+ parent.insertBefore(child.dom, pos);
339
+ }
320
340
  }
321
- let next = pos ? pos.nextSibling : parent.firstChild;
322
- if (next && track && track.node == parent)
341
+ if (pos && track && track.node == parent)
323
342
  track.written = true;
324
- while (next)
325
- next = rm(next);
343
+ while (pos)
344
+ pos = rm(pos);
326
345
  }
327
346
  else if (this.dirty & 1 /* Child */) {
328
347
  for (let child of this.children)
@@ -462,14 +481,6 @@ function rm(dom) {
462
481
  dom.parentNode.removeChild(dom);
463
482
  return next;
464
483
  }
465
- function syncNodeInto(parent, after, dom) {
466
- let next = after ? after.nextSibling : parent.firstChild;
467
- if (dom.parentNode == parent)
468
- while (next != dom)
469
- next = rm(next);
470
- else
471
- parent.insertBefore(dom, next);
472
- }
473
484
  class ChildCursor {
474
485
  constructor(children, pos, i) {
475
486
  this.children = children;
@@ -1556,6 +1567,9 @@ const inputHandler = /*@__PURE__*/Facet.define();
1556
1567
  const scrollTo = /*@__PURE__*/StateEffect.define({
1557
1568
  map: (range, changes) => range.map(changes)
1558
1569
  });
1570
+ const centerOn = /*@__PURE__*/StateEffect.define({
1571
+ map: (range, changes) => range.map(changes)
1572
+ });
1559
1573
  /**
1560
1574
  Log or report an unhandled exception in client code. Should
1561
1575
  probably only be used by extension code that allows client code to
@@ -2300,7 +2314,7 @@ class DocView extends ContentView {
2300
2314
  this.view.viewState.lineGapDeco
2301
2315
  ];
2302
2316
  }
2303
- scrollRangeIntoView(range) {
2317
+ scrollIntoView({ range, center }) {
2304
2318
  let rect = this.coordsAt(range.head, range.empty ? range.assoc : range.head > range.anchor ? -1 : 1), other;
2305
2319
  if (!rect)
2306
2320
  return;
@@ -2320,10 +2334,10 @@ class DocView extends ContentView {
2320
2334
  if (bottom != null)
2321
2335
  mBottom = Math.max(mBottom, bottom);
2322
2336
  }
2323
- scrollRectIntoView(this.dom, {
2337
+ scrollRectIntoView(this.view.scrollDOM, {
2324
2338
  left: rect.left - mLeft, top: rect.top - mTop,
2325
2339
  right: rect.right + mRight, bottom: rect.bottom + mBottom
2326
- }, range.head < range.anchor ? -1 : 1);
2340
+ }, range.head < range.anchor ? -1 : 1, center);
2327
2341
  }
2328
2342
  }
2329
2343
  function betweenUneditable(pos) {
@@ -3059,10 +3073,6 @@ class InputState {
3059
3073
  constructor(view) {
3060
3074
  this.lastKeyCode = 0;
3061
3075
  this.lastKeyTime = 0;
3062
- // On iOS, some keys need to have their default behavior happen
3063
- // (after which we retroactively handle them and reset the DOM) to
3064
- // avoid messing up the virtual keyboard state.
3065
- //
3066
3076
  // On Chrome Android, backspace near widgets is just completely
3067
3077
  // broken, and there are no key events, so we need to handle the
3068
3078
  // beforeinput event. Deleting stuff will often create a flurry of
@@ -3070,12 +3080,11 @@ class InputState {
3070
3080
  // subsequent events even more broken, so again, we hold off doing
3071
3081
  // anything until the browser is finished with whatever it is trying
3072
3082
  // to do.
3073
- //
3074
- // setPendingKey sets this, causing the DOM observer to pause for a
3075
- // bit, and setting an animation frame (which seems the most
3076
- // reliable way to detect 'browser is done flailing') to fire a fake
3077
- // key event and re-sync the view again.
3078
- this.pendingKey = undefined;
3083
+ this.pendingAndroidKey = undefined;
3084
+ // On iOS, some keys need to have their default behavior happen
3085
+ // (after which we retroactively handle them and reset the DOM) to
3086
+ // avoid messing up the virtual keyboard state.
3087
+ this.pendingIOSKey = undefined;
3079
3088
  this.lastSelectionOrigin = null;
3080
3089
  this.lastSelectionTime = 0;
3081
3090
  this.lastEscPress = 0;
@@ -3187,18 +3196,30 @@ class InputState {
3187
3196
  let pending;
3188
3197
  if (browser.ios && (pending = PendingKeys.find(key => key.keyCode == event.keyCode)) &&
3189
3198
  !(event.ctrlKey || event.altKey || event.metaKey) && !event.synthetic) {
3190
- this.setPendingKey(view, pending);
3199
+ this.pendingIOSKey = pending;
3200
+ setTimeout(() => this.flushIOSKey(view), 250);
3191
3201
  return true;
3192
3202
  }
3193
3203
  return false;
3194
3204
  }
3195
- setPendingKey(view, pending) {
3196
- this.pendingKey = pending;
3205
+ flushIOSKey(view) {
3206
+ let key = this.pendingIOSKey;
3207
+ if (!key)
3208
+ return false;
3209
+ this.pendingIOSKey = undefined;
3210
+ return dispatchKey(view.contentDOM, key.key, key.keyCode);
3211
+ }
3212
+ // This causes the DOM observer to pause for a bit, and sets an
3213
+ // animation frame (which seems the most reliable way to detect
3214
+ // 'Chrome is done flailing about messing with the DOM') to fire a
3215
+ // fake key event and re-sync the view again.
3216
+ setPendingAndroidKey(view, pending) {
3217
+ this.pendingAndroidKey = pending;
3197
3218
  requestAnimationFrame(() => {
3198
- if (!this.pendingKey)
3199
- return false;
3200
- let key = this.pendingKey;
3201
- this.pendingKey = undefined;
3219
+ let key = this.pendingAndroidKey;
3220
+ if (!key)
3221
+ return;
3222
+ this.pendingAndroidKey = undefined;
3202
3223
  view.observer.processRecords();
3203
3224
  let startState = view.state;
3204
3225
  dispatchKey(view.contentDOM, key.key, key.keyCode);
@@ -3712,18 +3733,20 @@ handlers.beforeinput = (view, event) => {
3712
3733
  // seems to do nothing at all on Chrome).
3713
3734
  let pending;
3714
3735
  if (browser.chrome && browser.android && (pending = PendingKeys.find(key => key.inputType == event.inputType))) {
3715
- view.inputState.setPendingKey(view, pending);
3716
- let startViewHeight = ((_a = window.visualViewport) === null || _a === void 0 ? void 0 : _a.height) || 0;
3717
- setTimeout(() => {
3718
- var _a;
3719
- // Backspacing near uneditable nodes on Chrome Android sometimes
3720
- // closes the virtual keyboard. This tries to crudely detect
3721
- // that and refocus to get it back.
3722
- if ((((_a = window.visualViewport) === null || _a === void 0 ? void 0 : _a.height) || 0) > startViewHeight + 10 && view.hasFocus) {
3723
- view.contentDOM.blur();
3724
- view.focus();
3725
- }
3726
- }, 50);
3736
+ view.inputState.setPendingAndroidKey(view, pending);
3737
+ if (pending.key == "Backspace" || pending.key == "Delete") {
3738
+ let startViewHeight = ((_a = window.visualViewport) === null || _a === void 0 ? void 0 : _a.height) || 0;
3739
+ setTimeout(() => {
3740
+ var _a;
3741
+ // Backspacing near uneditable nodes on Chrome Android sometimes
3742
+ // closes the virtual keyboard. This tries to crudely detect
3743
+ // that and refocus to get it back.
3744
+ if ((((_a = window.visualViewport) === null || _a === void 0 ? void 0 : _a.height) || 0) > startViewHeight + 10 && view.hasFocus) {
3745
+ view.contentDOM.blur();
3746
+ view.focus();
3747
+ }
3748
+ }, 100);
3749
+ }
3727
3750
  }
3728
3751
  };
3729
3752
 
@@ -4440,6 +4463,15 @@ class LineGapWidget extends WidgetType {
4440
4463
  }
4441
4464
  get estimatedHeight() { return this.vertical ? this.size : -1; }
4442
4465
  }
4466
+ class ScrollTarget {
4467
+ constructor(range, center = false) {
4468
+ this.range = range;
4469
+ this.center = center;
4470
+ }
4471
+ map(changes) {
4472
+ return changes.empty ? this : new ScrollTarget(this.range.map(changes), this.center);
4473
+ }
4474
+ }
4443
4475
  class ViewState {
4444
4476
  constructor(state) {
4445
4477
  this.state = state;
@@ -4452,7 +4484,7 @@ class ViewState {
4452
4484
  this.heightOracle = new HeightOracle;
4453
4485
  // See VP.MaxDOMHeight
4454
4486
  this.scaler = IdScaler;
4455
- this.scrollTo = null;
4487
+ this.scrollTarget = null;
4456
4488
  // Briefly set to true when printing, to disable viewport limiting
4457
4489
  this.printing = false;
4458
4490
  this.visibleRanges = [];
@@ -4485,7 +4517,7 @@ class ViewState {
4485
4517
  this.scaler = this.heightMap.height <= 7000000 /* MaxDOMHeight */ ? IdScaler :
4486
4518
  new BigScaler(this.heightOracle.doc, this.heightMap, this.viewports);
4487
4519
  }
4488
- update(update, scrollTo = null) {
4520
+ update(update, scrollTarget = null) {
4489
4521
  let prev = this.state;
4490
4522
  this.state = update.state;
4491
4523
  let newDeco = this.state.facet(decorations);
@@ -4496,15 +4528,16 @@ class ViewState {
4496
4528
  if (this.heightMap.height != prevHeight)
4497
4529
  update.flags |= 2 /* Height */;
4498
4530
  let viewport = heightChanges.length ? this.mapViewport(this.viewport, update.changes) : this.viewport;
4499
- if (scrollTo && (scrollTo.head < viewport.from || scrollTo.head > viewport.to) || !this.viewportIsAppropriate(viewport))
4500
- viewport = this.getViewport(0, scrollTo);
4531
+ if (scrollTarget && (scrollTarget.range.head < viewport.from || scrollTarget.range.head > viewport.to) ||
4532
+ !this.viewportIsAppropriate(viewport))
4533
+ viewport = this.getViewport(0, scrollTarget);
4501
4534
  this.viewport = viewport;
4502
4535
  this.updateForViewport();
4503
- if (this.lineGaps.length || this.viewport.to - this.viewport.from > 15000 /* MinViewPort */)
4536
+ if (this.lineGaps.length || this.viewport.to - this.viewport.from > 4000 /* DoubleMargin */)
4504
4537
  this.updateLineGaps(this.ensureLineGaps(this.mapLineGaps(this.lineGaps, update.changes)));
4505
4538
  update.flags |= this.computeVisibleRanges();
4506
- if (scrollTo)
4507
- this.scrollTo = scrollTo;
4539
+ if (scrollTarget)
4540
+ this.scrollTarget = scrollTarget;
4508
4541
  if (!this.mustEnforceCursorAssoc && update.selectionSet && update.view.lineWrapping &&
4509
4542
  update.state.selection.main.empty && update.state.selection.main.assoc)
4510
4543
  this.mustEnforceCursorAssoc = true;
@@ -4557,10 +4590,10 @@ class ViewState {
4557
4590
  if (oracle.heightChanged)
4558
4591
  result |= 2 /* Height */;
4559
4592
  if (!this.viewportIsAppropriate(this.viewport, bias) ||
4560
- this.scrollTo && (this.scrollTo.head < this.viewport.from || this.scrollTo.head > this.viewport.to))
4561
- this.viewport = this.getViewport(bias, this.scrollTo);
4593
+ this.scrollTarget && (this.scrollTarget.range.head < this.viewport.from || this.scrollTarget.range.head > this.viewport.to))
4594
+ this.viewport = this.getViewport(bias, this.scrollTarget);
4562
4595
  this.updateForViewport();
4563
- if (this.lineGaps.length || this.viewport.to - this.viewport.from > 15000 /* MinViewPort */)
4596
+ if (this.lineGaps.length || this.viewport.to - this.viewport.from > 4000 /* DoubleMargin */)
4564
4597
  this.updateLineGaps(this.ensureLineGaps(refresh ? [] : this.lineGaps));
4565
4598
  result |= this.computeVisibleRanges();
4566
4599
  if (this.mustEnforceCursorAssoc) {
@@ -4575,22 +4608,25 @@ class ViewState {
4575
4608
  }
4576
4609
  get visibleTop() { return this.scaler.fromDOM(this.pixelViewport.top, 0); }
4577
4610
  get visibleBottom() { return this.scaler.fromDOM(this.pixelViewport.bottom, 0); }
4578
- getViewport(bias, scrollTo) {
4611
+ getViewport(bias, scrollTarget) {
4579
4612
  // This will divide VP.Margin between the top and the
4580
4613
  // bottom, depending on the bias (the change in viewport position
4581
4614
  // since the last update). It'll hold a number between 0 and 1
4582
4615
  let marginTop = 0.5 - Math.max(-0.5, Math.min(0.5, bias / 1000 /* Margin */ / 2));
4583
4616
  let map = this.heightMap, doc = this.state.doc, { visibleTop, visibleBottom } = this;
4584
4617
  let viewport = new Viewport(map.lineAt(visibleTop - marginTop * 1000 /* Margin */, QueryType.ByHeight, doc, 0, 0).from, map.lineAt(visibleBottom + (1 - marginTop) * 1000 /* Margin */, QueryType.ByHeight, doc, 0, 0).to);
4585
- // If scrollTo is given, make sure the viewport includes that position
4586
- if (scrollTo) {
4587
- if (scrollTo.head < viewport.from) {
4588
- let { top: newTop } = map.lineAt(scrollTo.head, QueryType.ByPos, doc, 0, 0);
4589
- viewport = new Viewport(map.lineAt(newTop - 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).from, map.lineAt(newTop + (visibleBottom - visibleTop) + 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).to);
4590
- }
4591
- else if (scrollTo.head > viewport.to) {
4592
- let { bottom: newBottom } = map.lineAt(scrollTo.head, QueryType.ByPos, doc, 0, 0);
4593
- viewport = new Viewport(map.lineAt(newBottom - (visibleBottom - visibleTop) - 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).from, map.lineAt(newBottom + 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).to);
4618
+ // If scrollTarget is given, make sure the viewport includes that position
4619
+ if (scrollTarget) {
4620
+ let { head } = scrollTarget.range, viewHeight = visibleBottom - visibleTop;
4621
+ if (head < viewport.from || head > viewport.to) {
4622
+ let block = map.lineAt(head, QueryType.ByPos, doc, 0, 0), topPos;
4623
+ if (scrollTarget.center)
4624
+ topPos = (block.top + block.bottom) / 2 - viewHeight / 2;
4625
+ else if (head < viewport.from)
4626
+ topPos = block.top;
4627
+ else
4628
+ topPos = block.bottom - viewHeight;
4629
+ viewport = new Viewport(map.lineAt(topPos - 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).from, map.lineAt(topPos + viewHeight + 1000 /* Margin */ / 2, QueryType.ByHeight, doc, 0, 0).to);
4594
4630
  }
4595
4631
  }
4596
4632
  return viewport;
@@ -4632,52 +4668,50 @@ class ViewState {
4632
4668
  if (this.heightOracle.direction != Direction.LTR)
4633
4669
  return gaps;
4634
4670
  this.heightMap.forEachLine(this.viewport.from, this.viewport.to, this.state.doc, 0, 0, line => {
4635
- if (line.length < 10000 /* Margin */)
4671
+ if (line.length < 4000 /* DoubleMargin */)
4636
4672
  return;
4637
4673
  let structure = lineStructure(line.from, line.to, this.state);
4638
- if (structure.total < 10000 /* Margin */)
4674
+ if (structure.total < 4000 /* DoubleMargin */)
4639
4675
  return;
4640
4676
  let viewFrom, viewTo;
4641
4677
  if (this.heightOracle.lineWrapping) {
4642
- if (line.from != this.viewport.from)
4643
- viewFrom = line.from;
4644
- else
4645
- viewFrom = findPosition(structure, (this.visibleTop - line.top) / line.height);
4646
- if (line.to != this.viewport.to)
4647
- viewTo = line.to;
4648
- else
4649
- viewTo = findPosition(structure, (this.visibleBottom - line.top) / line.height);
4678
+ let marginHeight = (2000 /* Margin */ / this.heightOracle.lineLength) * this.heightOracle.lineHeight;
4679
+ viewFrom = findPosition(structure, (this.visibleTop - line.top - marginHeight) / line.height);
4680
+ viewTo = findPosition(structure, (this.visibleBottom - line.top + marginHeight) / line.height);
4650
4681
  }
4651
4682
  else {
4652
4683
  let totalWidth = structure.total * this.heightOracle.charWidth;
4653
- viewFrom = findPosition(structure, this.pixelViewport.left / totalWidth);
4654
- viewTo = findPosition(structure, this.pixelViewport.right / totalWidth);
4684
+ let marginWidth = 2000 /* Margin */ * this.heightOracle.charWidth;
4685
+ viewFrom = findPosition(structure, (this.pixelViewport.left - marginWidth) / totalWidth);
4686
+ viewTo = findPosition(structure, (this.pixelViewport.right + marginWidth) / totalWidth);
4655
4687
  }
4688
+ let outside = [];
4689
+ if (viewFrom > line.from)
4690
+ outside.push({ from: line.from, to: viewFrom });
4691
+ if (viewTo < line.to)
4692
+ outside.push({ from: viewTo, to: line.to });
4656
4693
  let sel = this.state.selection.main;
4657
- // Make sure the gap doesn't cover a selection end
4658
- if (sel.from <= viewFrom && sel.to >= line.from)
4659
- viewFrom = sel.from;
4660
- if (sel.from <= line.to && sel.to >= viewTo)
4661
- viewTo = sel.to;
4662
- let gapTo = viewFrom - 10000 /* Margin */, gapFrom = viewTo + 10000 /* Margin */;
4663
- if (gapTo > line.from + 5000 /* HalfMargin */)
4664
- gaps.push(find(current, gap => gap.from == line.from && gap.to > gapTo - 5000 /* HalfMargin */ && gap.to < gapTo + 5000 /* HalfMargin */) ||
4665
- new LineGap(line.from, gapTo, this.gapSize(line, gapTo, true, structure)));
4666
- if (gapFrom < line.to - 5000 /* HalfMargin */)
4667
- gaps.push(find(current, gap => gap.to == line.to && gap.from > gapFrom - 5000 /* HalfMargin */ &&
4668
- gap.from < gapFrom + 5000 /* HalfMargin */) ||
4669
- new LineGap(gapFrom, line.to, this.gapSize(line, gapFrom, false, structure)));
4694
+ // Make sure the gaps don't cover a selection end
4695
+ if (sel.from >= line.from && sel.from <= line.to)
4696
+ cutRange(outside, sel.from - 10 /* SelectionMargin */, sel.from + 10 /* SelectionMargin */);
4697
+ if (!sel.empty && sel.to >= line.from && sel.to <= line.to)
4698
+ cutRange(outside, sel.to - 10 /* SelectionMargin */, sel.to + 10 /* SelectionMargin */);
4699
+ for (let { from, to } of outside)
4700
+ if (to - from > 1000 /* HalfMargin */) {
4701
+ gaps.push(find(current, gap => gap.from >= line.from && gap.to <= line.to &&
4702
+ Math.abs(gap.from - from) < 1000 /* HalfMargin */ && Math.abs(gap.to - to) < 1000 /* HalfMargin */) ||
4703
+ new LineGap(from, to, this.gapSize(line, from, to, structure)));
4704
+ }
4670
4705
  });
4671
4706
  return gaps;
4672
4707
  }
4673
- gapSize(line, pos, start, structure) {
4708
+ gapSize(line, from, to, structure) {
4709
+ let fraction = findFraction(structure, to) - findFraction(structure, from);
4674
4710
  if (this.heightOracle.lineWrapping) {
4675
- let height = line.height * findFraction(structure, pos);
4676
- return start ? height : line.height - height;
4711
+ return line.height * fraction;
4677
4712
  }
4678
4713
  else {
4679
- let ratio = findFraction(structure, pos);
4680
- return structure.total * this.heightOracle.charWidth * (start ? ratio : 1 - ratio);
4714
+ return structure.total * this.heightOracle.charWidth * fraction;
4681
4715
  }
4682
4716
  }
4683
4717
  updateLineGaps(gaps) {
@@ -4771,6 +4805,20 @@ function findFraction(structure, pos) {
4771
4805
  }
4772
4806
  return counted / structure.total;
4773
4807
  }
4808
+ function cutRange(ranges, from, to) {
4809
+ for (let i = 0; i < ranges.length; i++) {
4810
+ let r = ranges[i];
4811
+ if (r.from < to && r.to > from) {
4812
+ let pieces = [];
4813
+ if (r.from < from)
4814
+ pieces.push({ from: r.from, to: from });
4815
+ if (r.to > to)
4816
+ pieces.push({ from: to, to: r.to });
4817
+ ranges.splice(i, 1, ...pieces);
4818
+ i += pieces.length - 1;
4819
+ }
4820
+ }
4821
+ }
4774
4822
  function find(array, f) {
4775
4823
  for (let val of array)
4776
4824
  if (f(val))
@@ -5232,7 +5280,7 @@ class DOMObserver {
5232
5280
  // Completely hold off flushing when pending keys are set—the code
5233
5281
  // managing those will make sure processRecords is called and the
5234
5282
  // view is resynchronized after
5235
- if (this.delayedFlush >= 0 || this.view.inputState.pendingKey)
5283
+ if (this.delayedFlush >= 0 || this.view.inputState.pendingAndroidKey)
5236
5284
  return;
5237
5285
  this.lastFlush = Date.now();
5238
5286
  let { from, to, typeOver } = this.processRecords();
@@ -5319,8 +5367,11 @@ function safariSelectionRangeHack(view) {
5319
5367
 
5320
5368
  function applyDOMChange(view, start, end, typeOver) {
5321
5369
  let change, newSel;
5322
- let sel = view.state.selection.main, bounds;
5323
- if (start > -1 && !view.state.readOnly && (bounds = view.docView.domBoundsAround(start, end, 0))) {
5370
+ let sel = view.state.selection.main;
5371
+ if (start > -1) {
5372
+ let bounds = view.docView.domBoundsAround(start, end, 0);
5373
+ if (!bounds || view.state.readOnly)
5374
+ return;
5324
5375
  let { from, to } = bounds;
5325
5376
  let selPoints = view.docView.impreciseHead || view.docView.impreciseAnchor ? [] : selectionPoints(view);
5326
5377
  let reader = new DOMReader(selPoints, view);
@@ -5374,16 +5425,8 @@ function applyDOMChange(view, start, end, typeOver) {
5374
5425
  // backspace, or delete. So this detects changes that look like
5375
5426
  // they're caused by those keys, and reinterprets them as key
5376
5427
  // events.
5377
- if (browser.android &&
5378
- ((change.from == sel.from && change.to == sel.to &&
5379
- change.insert.length == 1 && change.insert.lines == 2 &&
5380
- dispatchKey(view.contentDOM, "Enter", 13)) ||
5381
- (change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 &&
5382
- dispatchKey(view.contentDOM, "Backspace", 8)) ||
5383
- (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
5384
- dispatchKey(view.contentDOM, "Delete", 46)))) {
5428
+ if (browser.ios && view.inputState.flushIOSKey(view))
5385
5429
  return;
5386
- }
5387
5430
  let text = change.insert.toString();
5388
5431
  if (view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text)))
5389
5432
  return;
@@ -5685,21 +5728,24 @@ class EditorView {
5685
5728
  if (state.facet(EditorState.phrases) != this.state.facet(EditorState.phrases))
5686
5729
  return this.setState(state);
5687
5730
  update = new ViewUpdate(this, state, transactions);
5688
- let scrollPos = null;
5731
+ let scrollTarget = null;
5689
5732
  try {
5690
5733
  this.updateState = 2 /* Updating */;
5691
5734
  for (let tr of transactions) {
5692
- if (scrollPos)
5693
- scrollPos = scrollPos.map(tr.changes);
5735
+ if (scrollTarget)
5736
+ scrollTarget = scrollTarget.map(tr.changes);
5694
5737
  if (tr.scrollIntoView) {
5695
5738
  let { main } = tr.state.selection;
5696
- scrollPos = main.empty ? main : EditorSelection.cursor(main.head, main.head > main.anchor ? -1 : 1);
5739
+ scrollTarget = new ScrollTarget(main.empty ? main : EditorSelection.cursor(main.head, main.head > main.anchor ? -1 : 1));
5697
5740
  }
5698
- for (let e of tr.effects)
5741
+ for (let e of tr.effects) {
5699
5742
  if (e.is(scrollTo))
5700
- scrollPos = e.value;
5743
+ scrollTarget = new ScrollTarget(e.value);
5744
+ else if (e.is(centerOn))
5745
+ scrollTarget = new ScrollTarget(e.value, true);
5746
+ }
5701
5747
  }
5702
- this.viewState.update(update, scrollPos);
5748
+ this.viewState.update(update, scrollTarget);
5703
5749
  this.bidiCache = CachedOrder.update(this.bidiCache, update.changes);
5704
5750
  if (!update.empty) {
5705
5751
  this.updatePlugins(update);
@@ -5714,7 +5760,7 @@ class EditorView {
5714
5760
  finally {
5715
5761
  this.updateState = 0 /* Idle */;
5716
5762
  }
5717
- if (redrawn || scrollPos || this.viewState.mustEnforceCursorAssoc)
5763
+ if (redrawn || scrollTarget || this.viewState.mustEnforceCursorAssoc)
5718
5764
  this.requestMeasure();
5719
5765
  if (!update.empty)
5720
5766
  for (let listener of this.state.facet(updateListener))
@@ -5796,7 +5842,7 @@ class EditorView {
5796
5842
  this.updateState = 1 /* Measuring */;
5797
5843
  let oldViewport = this.viewport;
5798
5844
  let changed = this.viewState.measure(this.docView, i > 0);
5799
- if (!changed && !this.measureRequests.length && this.viewState.scrollTo == null)
5845
+ if (!changed && !this.measureRequests.length && this.viewState.scrollTarget == null)
5800
5846
  break;
5801
5847
  if (i > 5) {
5802
5848
  console.warn("Viewport failed to stabilize");
@@ -5838,9 +5884,9 @@ class EditorView {
5838
5884
  logException(this.state, e);
5839
5885
  }
5840
5886
  }
5841
- if (this.viewState.scrollTo) {
5842
- this.docView.scrollRangeIntoView(this.viewState.scrollTo);
5843
- this.viewState.scrollTo = null;
5887
+ if (this.viewState.scrollTarget) {
5888
+ this.docView.scrollIntoView(this.viewState.scrollTarget);
5889
+ this.viewState.scrollTarget = null;
5844
5890
  }
5845
5891
  if (this.viewport.from == oldViewport.from && this.viewport.to == oldViewport.to && this.measureRequests.length == 0)
5846
5892
  break;
@@ -6063,12 +6109,9 @@ class EditorView {
6063
6109
  moveVertically(start, forward, distance) {
6064
6110
  return skipAtoms(this, start, moveVertically(this, start, forward, distance));
6065
6111
  }
6066
- /**
6067
- Scroll the given document position into view.
6068
- */
6112
+ // FIXME remove on next major version
6069
6113
  scrollPosIntoView(pos) {
6070
- this.viewState.scrollTo = EditorSelection.cursor(pos);
6071
- this.requestMeasure();
6114
+ this.dispatch({ effects: scrollTo.of(EditorSelection.cursor(pos)) });
6072
6115
  }
6073
6116
  /**
6074
6117
  Find the DOM parent node and offset (child offset if `node` is
@@ -6235,7 +6278,7 @@ class EditorView {
6235
6278
  target editors with a dark or light theme.
6236
6279
  */
6237
6280
  static baseTheme(spec) {
6238
- return Prec.fallback(styleModule.of(buildTheme("." + baseThemeID, spec, lightDarkIDs)));
6281
+ return Prec.lowest(styleModule.of(buildTheme("." + baseThemeID, spec, lightDarkIDs)));
6239
6282
  }
6240
6283
  }
6241
6284
  /**
@@ -6244,6 +6287,11 @@ transaction to make it scroll the given range into view.
6244
6287
  */
6245
6288
  EditorView.scrollTo = scrollTo;
6246
6289
  /**
6290
+ Effect that makes the editor scroll the given range to the
6291
+ center of the visible view.
6292
+ */
6293
+ EditorView.centerOn = centerOn;
6294
+ /**
6247
6295
  Facet to add a [style
6248
6296
  module](https://github.com/marijnh/style-mod#documentation) to
6249
6297
  an editor view. The view will ensure that the module is
@@ -6672,7 +6720,7 @@ const themeSpec = {
6672
6720
  };
6673
6721
  if (CanHidePrimary)
6674
6722
  themeSpec[".cm-line"].caretColor = "transparent !important";
6675
- const hideNativeSelection = /*@__PURE__*/Prec.override(/*@__PURE__*/EditorView.theme(themeSpec));
6723
+ const hideNativeSelection = /*@__PURE__*/Prec.highest(/*@__PURE__*/EditorView.theme(themeSpec));
6676
6724
  function getBase(view) {
6677
6725
  let rect = view.scrollDOM.getBoundingClientRect();
6678
6726
  let left = view.textDirection == Direction.LTR ? rect.left : rect.right - view.scrollDOM.clientWidth;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "0.19.10",
3
+ "version": "0.19.14",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",
@@ -27,7 +27,7 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@codemirror/rangeset": "^0.19.0",
30
- "@codemirror/state": "^0.19.2",
30
+ "@codemirror/state": "^0.19.3",
31
31
  "@codemirror/text": "^0.19.0",
32
32
  "style-mod": "^4.0.0",
33
33
  "w3c-keyname": "^2.2.4"