@codemirror/view 6.2.5 → 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.cjs CHANGED
@@ -875,7 +875,7 @@ class WidgetView extends ContentView {
875
875
  if (pos > 0 ? i == 0 : i == rects.length - 1 || rect.top < rect.bottom)
876
876
  break;
877
877
  }
878
- return (pos == 0 && side > 0 || pos == this.length && side <= 0) ? rect : flattenRect(rect, pos == 0);
878
+ return flattenRect(rect, this.side > 0);
879
879
  }
880
880
  get isEditable() { return false; }
881
881
  destroy() {
@@ -1028,7 +1028,6 @@ function inlineDOMAtPos(parent, pos) {
1028
1028
  break;
1029
1029
  off = end;
1030
1030
  }
1031
- // if (i) return DOMPos.after(children[i - 1].dom!)
1032
1031
  for (let j = i; j > 0; j--) {
1033
1032
  let prev = children[j - 1];
1034
1033
  if (prev.dom.parentNode == dom)
@@ -1055,21 +1054,59 @@ function joinInlineInto(parent, view, open) {
1055
1054
  parent.length += view.length;
1056
1055
  }
1057
1056
  function coordsInChildren(view, pos, side) {
1058
- for (let off = 0, i = 0; i < view.children.length; i++) {
1059
- let child = view.children[i], end = off + child.length, next;
1060
- if ((side <= 0 || end == view.length || child.getSide() > 0 ? end >= pos : end > pos) &&
1061
- (pos < end || i + 1 == view.children.length || (next = view.children[i + 1]).length || next.getSide() > 0)) {
1062
- let flatten = 0;
1063
- if (end == off) {
1064
- if (child.getSide() <= 0)
1065
- continue;
1066
- flatten = side = -child.getSide();
1057
+ if (!view.children.length)
1058
+ return fallbackRect(view);
1059
+ return (side <= 0 ? coordsInChildrenBefore : coordsInChildrenAfter)(view, pos);
1060
+ }
1061
+ function coordsInChildrenBefore(view, pos) {
1062
+ // Find the last leaf in the tree that touches pos and doesn't have getSide() > 0
1063
+ let found = null, foundPos = -1;
1064
+ function scan(view, pos) {
1065
+ for (let i = 0, off = 0; i < view.children.length && off <= pos; i++) {
1066
+ let child = view.children[i], end = off + child.length;
1067
+ if (end >= pos) {
1068
+ if (child.children.length) {
1069
+ if (scan(child, pos - off))
1070
+ return true;
1071
+ }
1072
+ else if (end >= pos) {
1073
+ if (end == pos && child.getSide() > 0)
1074
+ return true;
1075
+ found = child;
1076
+ foundPos = pos - off;
1077
+ }
1078
+ }
1079
+ off = end;
1080
+ }
1081
+ }
1082
+ scan(view, pos);
1083
+ return found ? found.coordsAt(Math.max(0, foundPos), -1) : coordsInChildrenAfter(view, pos);
1084
+ }
1085
+ function coordsInChildrenAfter(view, pos) {
1086
+ // Find the first leaf in the tree that touches pos and doesn't have getSide() < 0
1087
+ let found = null, foundPos = -1;
1088
+ function scan(view, pos) {
1089
+ for (let i = view.children.length - 1, off = view.length; i >= 0 && off >= pos; i--) {
1090
+ let child = view.children[i];
1091
+ off -= child.length;
1092
+ if (off <= pos) {
1093
+ if (child.children.length) {
1094
+ if (scan(child, pos - off))
1095
+ return true;
1096
+ }
1097
+ else if (off <= pos) {
1098
+ if (off == pos && child.getSide() < 0)
1099
+ return true;
1100
+ found = child;
1101
+ foundPos = pos - off;
1102
+ }
1067
1103
  }
1068
- let rect = child.coordsAt(Math.max(0, pos - off), side);
1069
- return flatten && rect ? flattenRect(rect, side < 0) : rect;
1070
1104
  }
1071
- off = end;
1072
1105
  }
1106
+ scan(view, pos);
1107
+ return found ? found.coordsAt(Math.max(0, foundPos), 1) : coordsInChildrenBefore(view, pos);
1108
+ }
1109
+ function fallbackRect(view) {
1073
1110
  let last = view.dom.lastChild;
1074
1111
  if (!last)
1075
1112
  return view.dom.getBoundingClientRect();
@@ -1688,7 +1725,7 @@ class ContentBuilder {
1688
1725
  this.addBlockWidget(new BlockWidgetView(deco.widget || new NullWidget("div"), len, type));
1689
1726
  }
1690
1727
  else {
1691
- let view = WidgetView.create(deco.widget || new NullWidget("span"), len, deco.startSide);
1728
+ let view = WidgetView.create(deco.widget || new NullWidget("span"), len, len ? 0 : deco.startSide);
1692
1729
  let cursorBefore = this.atCursorPos && !view.isEditable && openStart <= active.length && (from < to || deco.startSide > 0);
1693
1730
  let cursorAfter = !view.isEditable && (from < to || deco.startSide <= 0);
1694
1731
  let line = this.getLine();
@@ -4827,7 +4864,7 @@ class ViewState {
4827
4864
  this.updateForViewport();
4828
4865
  if (updateLines)
4829
4866
  this.updateViewportLines();
4830
- if (this.lineGaps.length || this.viewport.to - this.viewport.from > 4000 /* LG.DoubleMargin */)
4867
+ if (this.lineGaps.length || this.viewport.to - this.viewport.from > (2000 /* LG.Margin */ << 1))
4831
4868
  this.updateLineGaps(this.ensureLineGaps(this.mapLineGaps(this.lineGaps, update.changes)));
4832
4869
  update.flags |= this.computeVisibleRanges();
4833
4870
  if (scrollTarget)
@@ -4883,7 +4920,7 @@ class ViewState {
4883
4920
  refresh = true;
4884
4921
  if (refresh || oracle.lineWrapping && Math.abs(contentWidth - this.contentDOMWidth) > oracle.charWidth) {
4885
4922
  let { lineHeight, charWidth } = view.docView.measureTextSize();
4886
- refresh = oracle.refresh(whiteSpace, lineHeight, charWidth, contentWidth / charWidth, lineHeights);
4923
+ refresh = lineHeight > 0 && oracle.refresh(whiteSpace, lineHeight, charWidth, contentWidth / charWidth, lineHeights);
4887
4924
  if (refresh) {
4888
4925
  view.docView.minWidth = 0;
4889
4926
  result |= 8 /* UpdateFlag.Geometry */;
@@ -4908,8 +4945,8 @@ class ViewState {
4908
4945
  this.updateForViewport();
4909
4946
  if ((result & 2 /* UpdateFlag.Height */) || viewportChange)
4910
4947
  this.updateViewportLines();
4911
- if (this.lineGaps.length || this.viewport.to - this.viewport.from > 4000 /* LG.DoubleMargin */)
4912
- this.updateLineGaps(this.ensureLineGaps(refresh ? [] : this.lineGaps));
4948
+ if (this.lineGaps.length || this.viewport.to - this.viewport.from > (2000 /* LG.Margin */ << 1))
4949
+ this.updateLineGaps(this.ensureLineGaps(refresh ? [] : this.lineGaps, view));
4913
4950
  result |= this.computeVisibleRanges();
4914
4951
  if (this.mustEnforceCursorAssoc) {
4915
4952
  this.mustEnforceCursorAssoc = false;
@@ -4980,46 +5017,86 @@ class ViewState {
4980
5017
  // since actual DOM coordinates aren't always available and
4981
5018
  // predictable. Relies on generous margins (see LG.Margin) to hide
4982
5019
  // the artifacts this might produce from the user.
4983
- ensureLineGaps(current) {
5020
+ ensureLineGaps(current, mayMeasure) {
5021
+ let wrapping = this.heightOracle.lineWrapping;
5022
+ let margin = wrapping ? 10000 /* LG.MarginWrap */ : 2000 /* LG.Margin */, halfMargin = margin >> 1, doubleMargin = margin << 1;
5023
+ // The non-wrapping logic won't work at all in predominantly right-to-left text.
5024
+ if (this.defaultTextDirection != exports.Direction.LTR && !wrapping)
5025
+ return [];
4984
5026
  let gaps = [];
4985
- // This won't work at all in predominantly right-to-left text.
4986
- if (this.defaultTextDirection != exports.Direction.LTR)
4987
- return gaps;
5027
+ let addGap = (from, to, line, structure) => {
5028
+ if (to - from < halfMargin)
5029
+ return;
5030
+ let sel = this.state.selection.main, avoid = [sel.from];
5031
+ if (!sel.empty)
5032
+ avoid.push(sel.to);
5033
+ for (let pos of avoid) {
5034
+ if (pos > from && pos < to) {
5035
+ addGap(from, pos - 10 /* LG.SelectionMargin */, line, structure);
5036
+ addGap(pos + 10 /* LG.SelectionMargin */, to, line, structure);
5037
+ return;
5038
+ }
5039
+ }
5040
+ let gap = find(current, gap => gap.from >= line.from && gap.to <= line.to &&
5041
+ Math.abs(gap.from - from) < halfMargin && Math.abs(gap.to - to) < halfMargin &&
5042
+ !avoid.some(pos => gap.from < pos && gap.to > pos));
5043
+ if (!gap) {
5044
+ // When scrolling down, snap gap ends to line starts to avoid shifts in wrapping
5045
+ if (to < line.to && mayMeasure && wrapping &&
5046
+ mayMeasure.visibleRanges.some(r => r.from <= to && r.to >= to)) {
5047
+ let lineStart = mayMeasure.moveToLineBoundary(state.EditorSelection.cursor(to), false, true).head;
5048
+ if (lineStart > from)
5049
+ to = lineStart;
5050
+ }
5051
+ gap = new LineGap(from, to, this.gapSize(line, from, to, structure));
5052
+ }
5053
+ gaps.push(gap);
5054
+ };
4988
5055
  for (let line of this.viewportLines) {
4989
- if (line.length < 4000 /* LG.DoubleMargin */)
5056
+ if (line.length < doubleMargin)
4990
5057
  continue;
4991
5058
  let structure = lineStructure(line.from, line.to, this.stateDeco);
4992
- if (structure.total < 4000 /* LG.DoubleMargin */)
5059
+ if (structure.total < doubleMargin)
4993
5060
  continue;
5061
+ let target = this.scrollTarget ? this.scrollTarget.range.head : null;
4994
5062
  let viewFrom, viewTo;
4995
- if (this.heightOracle.lineWrapping) {
4996
- let marginHeight = (2000 /* LG.Margin */ / this.heightOracle.lineLength) * this.heightOracle.lineHeight;
4997
- viewFrom = findPosition(structure, (this.visibleTop - line.top - marginHeight) / line.height);
4998
- viewTo = findPosition(structure, (this.visibleBottom - line.top + marginHeight) / line.height);
5063
+ if (wrapping) {
5064
+ let marginHeight = (margin / this.heightOracle.lineLength) * this.heightOracle.lineHeight;
5065
+ let top, bot;
5066
+ if (target != null) {
5067
+ let targetFrac = findFraction(structure, target);
5068
+ let spaceFrac = ((this.visibleBottom - this.visibleTop) / 2 + marginHeight) / line.height;
5069
+ top = targetFrac - spaceFrac;
5070
+ bot = targetFrac + spaceFrac;
5071
+ }
5072
+ else {
5073
+ top = (this.visibleTop - line.top - marginHeight) / line.height;
5074
+ bot = (this.visibleBottom - line.top + marginHeight) / line.height;
5075
+ }
5076
+ viewFrom = findPosition(structure, top);
5077
+ viewTo = findPosition(structure, bot);
4999
5078
  }
5000
5079
  else {
5001
5080
  let totalWidth = structure.total * this.heightOracle.charWidth;
5002
- let marginWidth = 2000 /* LG.Margin */ * this.heightOracle.charWidth;
5003
- viewFrom = findPosition(structure, (this.pixelViewport.left - marginWidth) / totalWidth);
5004
- viewTo = findPosition(structure, (this.pixelViewport.right + marginWidth) / totalWidth);
5081
+ let marginWidth = margin * this.heightOracle.charWidth;
5082
+ let left, right;
5083
+ if (target != null) {
5084
+ let targetFrac = findFraction(structure, target);
5085
+ let spaceFrac = ((this.pixelViewport.right - this.pixelViewport.left) / 2 + marginWidth) / totalWidth;
5086
+ left = targetFrac - spaceFrac;
5087
+ right = targetFrac + spaceFrac;
5088
+ }
5089
+ else {
5090
+ left = (this.pixelViewport.left - marginWidth) / totalWidth;
5091
+ right = (this.pixelViewport.right + marginWidth) / totalWidth;
5092
+ }
5093
+ viewFrom = findPosition(structure, left);
5094
+ viewTo = findPosition(structure, right);
5005
5095
  }
5006
- let outside = [];
5007
5096
  if (viewFrom > line.from)
5008
- outside.push({ from: line.from, to: viewFrom });
5097
+ addGap(line.from, viewFrom, line, structure);
5009
5098
  if (viewTo < line.to)
5010
- outside.push({ from: viewTo, to: line.to });
5011
- let sel = this.state.selection.main;
5012
- // Make sure the gaps don't cover a selection end
5013
- if (sel.from >= line.from && sel.from <= line.to)
5014
- cutRange(outside, sel.from - 10 /* LG.SelectionMargin */, sel.from + 10 /* LG.SelectionMargin */);
5015
- if (!sel.empty && sel.to >= line.from && sel.to <= line.to)
5016
- cutRange(outside, sel.to - 10 /* LG.SelectionMargin */, sel.to + 10 /* LG.SelectionMargin */);
5017
- for (let { from, to } of outside)
5018
- if (to - from > 1000 /* LG.HalfMargin */) {
5019
- gaps.push(find(current, gap => gap.from >= line.from && gap.to <= line.to &&
5020
- Math.abs(gap.from - from) < 1000 /* LG.HalfMargin */ && Math.abs(gap.to - to) < 1000 /* LG.HalfMargin */) ||
5021
- new LineGap(from, to, this.gapSize(line, from, to, structure)));
5022
- }
5099
+ addGap(viewTo, line.to, line, structure);
5023
5100
  }
5024
5101
  return gaps;
5025
5102
  }
@@ -5117,20 +5194,6 @@ function findFraction(structure, pos) {
5117
5194
  }
5118
5195
  return counted / structure.total;
5119
5196
  }
5120
- function cutRange(ranges, from, to) {
5121
- for (let i = 0; i < ranges.length; i++) {
5122
- let r = ranges[i];
5123
- if (r.from < to && r.to > from) {
5124
- let pieces = [];
5125
- if (r.from < from)
5126
- pieces.push({ from: r.from, to: from });
5127
- if (r.to > to)
5128
- pieces.push({ from: to, to: r.to });
5129
- ranges.splice(i, 1, ...pieces);
5130
- i += pieces.length - 1;
5131
- }
5132
- }
5133
- }
5134
5197
  function find(array, f) {
5135
5198
  for (let val of array)
5136
5199
  if (f(val))
@@ -5430,6 +5493,227 @@ const baseTheme$1 = buildTheme("." + baseThemeID, {
5430
5493
  }
5431
5494
  }, lightDarkIDs);
5432
5495
 
5496
+ class DOMChange {
5497
+ constructor(view, start, end, typeOver) {
5498
+ this.typeOver = typeOver;
5499
+ this.bounds = null;
5500
+ this.text = "";
5501
+ let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
5502
+ if (start > -1 && !view.state.readOnly && (this.bounds = view.docView.domBoundsAround(start, end, 0))) {
5503
+ let selPoints = iHead || iAnchor ? [] : selectionPoints(view);
5504
+ let reader = new DOMReader(selPoints, view.state);
5505
+ reader.readRange(this.bounds.startDOM, this.bounds.endDOM);
5506
+ this.text = reader.text;
5507
+ this.newSel = selectionFromPoints(selPoints, this.bounds.from);
5508
+ }
5509
+ else {
5510
+ let domSel = view.observer.selectionRange;
5511
+ let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ||
5512
+ !contains(view.contentDOM, domSel.focusNode)
5513
+ ? view.state.selection.main.head
5514
+ : view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
5515
+ let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset ||
5516
+ !contains(view.contentDOM, domSel.anchorNode)
5517
+ ? view.state.selection.main.anchor
5518
+ : view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
5519
+ this.newSel = state.EditorSelection.single(anchor, head);
5520
+ }
5521
+ }
5522
+ }
5523
+ function applyDOMChange(view, domChange) {
5524
+ let change;
5525
+ let { newSel } = domChange, sel = view.state.selection.main;
5526
+ if (domChange.bounds) {
5527
+ let { from, to } = domChange.bounds;
5528
+ let preferredPos = sel.from, preferredSide = null;
5529
+ // Prefer anchoring to end when Backspace is pressed (or, on
5530
+ // Android, when something was deleted)
5531
+ if (view.inputState.lastKeyCode === 8 && view.inputState.lastKeyTime > Date.now() - 100 ||
5532
+ browser.android && domChange.text.length < to - from) {
5533
+ preferredPos = sel.to;
5534
+ preferredSide = "end";
5535
+ }
5536
+ let diff = findDiff(view.state.doc.sliceString(from, to, LineBreakPlaceholder), domChange.text, preferredPos - from, preferredSide);
5537
+ if (diff) {
5538
+ // Chrome inserts two newlines when pressing shift-enter at the
5539
+ // end of a line. DomChange drops one of those.
5540
+ if (browser.chrome && view.inputState.lastKeyCode == 13 &&
5541
+ diff.toB == diff.from + 2 && domChange.text.slice(diff.from, diff.toB) == LineBreakPlaceholder + LineBreakPlaceholder)
5542
+ diff.toB--;
5543
+ change = { from: from + diff.from, to: from + diff.toA,
5544
+ insert: state.Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
5545
+ }
5546
+ }
5547
+ else if (newSel && (!view.hasFocus || !view.state.facet(editable) || newSel.main.eq(sel))) {
5548
+ newSel = null;
5549
+ }
5550
+ if (!change && !newSel)
5551
+ return false;
5552
+ if (!change && domChange.typeOver && !sel.empty && newSel && newSel.main.empty) {
5553
+ // Heuristic to notice typing over a selected character
5554
+ change = { from: sel.from, to: sel.to, insert: view.state.doc.slice(sel.from, sel.to) };
5555
+ }
5556
+ else if (change && change.from >= sel.from && change.to <= sel.to &&
5557
+ (change.from != sel.from || change.to != sel.to) &&
5558
+ (sel.to - sel.from) - (change.to - change.from) <= 4) {
5559
+ // If the change is inside the selection and covers most of it,
5560
+ // assume it is a selection replace (with identical characters at
5561
+ // the start/end not included in the diff)
5562
+ change = {
5563
+ from: sel.from, to: sel.to,
5564
+ insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
5565
+ };
5566
+ }
5567
+ else if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
5568
+ /^\. ?$/.test(change.insert.toString())) {
5569
+ // Detect insert-period-on-double-space Mac and Android behavior,
5570
+ // and transform it into a regular space insert.
5571
+ if (newSel && change.insert.length == 2)
5572
+ newSel = state.EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
5573
+ change = { from: sel.from, to: sel.to, insert: state.Text.of([" "]) };
5574
+ }
5575
+ if (change) {
5576
+ let startState = view.state;
5577
+ if (browser.ios && view.inputState.flushIOSKey(view))
5578
+ return true;
5579
+ // Android browsers don't fire reasonable key events for enter,
5580
+ // backspace, or delete. So this detects changes that look like
5581
+ // they're caused by those keys, and reinterprets them as key
5582
+ // events. (Some of these keys are also handled by beforeinput
5583
+ // events and the pendingAndroidKey mechanism, but that's not
5584
+ // reliable in all situations.)
5585
+ if (browser.android &&
5586
+ ((change.from == sel.from && change.to == sel.to &&
5587
+ change.insert.length == 1 && change.insert.lines == 2 &&
5588
+ dispatchKey(view.contentDOM, "Enter", 13)) ||
5589
+ (change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 &&
5590
+ dispatchKey(view.contentDOM, "Backspace", 8)) ||
5591
+ (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
5592
+ dispatchKey(view.contentDOM, "Delete", 46))))
5593
+ return true;
5594
+ let text = change.insert.toString();
5595
+ if (view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text)))
5596
+ return true;
5597
+ if (view.inputState.composing >= 0)
5598
+ view.inputState.composing++;
5599
+ let tr;
5600
+ if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
5601
+ (!newSel || newSel.main.empty && newSel.main.from == change.from + change.insert.length) &&
5602
+ view.inputState.composing < 0) {
5603
+ let before = sel.from < change.from ? startState.sliceDoc(sel.from, change.from) : "";
5604
+ let after = sel.to > change.to ? startState.sliceDoc(change.to, sel.to) : "";
5605
+ tr = startState.replaceSelection(view.state.toText(before + change.insert.sliceString(0, undefined, view.state.lineBreak) + after));
5606
+ }
5607
+ else {
5608
+ let changes = startState.changes(change);
5609
+ let mainSel = newSel && !startState.selection.main.eq(newSel.main) && newSel.main.to <= changes.newLength
5610
+ ? newSel.main : undefined;
5611
+ // Try to apply a composition change to all cursors
5612
+ if (startState.selection.ranges.length > 1 && view.inputState.composing >= 0 &&
5613
+ change.to <= sel.to && change.to >= sel.to - 10) {
5614
+ let replaced = view.state.sliceDoc(change.from, change.to);
5615
+ let compositionRange = compositionSurroundingNode(view) || view.state.doc.lineAt(sel.head);
5616
+ let offset = sel.to - change.to, size = sel.to - sel.from;
5617
+ tr = startState.changeByRange(range => {
5618
+ if (range.from == sel.from && range.to == sel.to)
5619
+ return { changes, range: mainSel || range.map(changes) };
5620
+ let to = range.to - offset, from = to - replaced.length;
5621
+ if (range.to - range.from != size || view.state.sliceDoc(from, to) != replaced ||
5622
+ // Unfortunately, there's no way to make multiple
5623
+ // changes in the same node work without aborting
5624
+ // composition, so cursors in the composition range are
5625
+ // ignored.
5626
+ compositionRange && range.to >= compositionRange.from && range.from <= compositionRange.to)
5627
+ return { range };
5628
+ let rangeChanges = startState.changes({ from, to, insert: change.insert }), selOff = range.to - sel.to;
5629
+ return {
5630
+ changes: rangeChanges,
5631
+ range: !mainSel ? range.map(rangeChanges) :
5632
+ state.EditorSelection.range(Math.max(0, mainSel.anchor + selOff), Math.max(0, mainSel.head + selOff))
5633
+ };
5634
+ });
5635
+ }
5636
+ else {
5637
+ tr = {
5638
+ changes,
5639
+ selection: mainSel && startState.selection.replaceRange(mainSel)
5640
+ };
5641
+ }
5642
+ }
5643
+ let userEvent = "input.type";
5644
+ if (view.composing) {
5645
+ userEvent += ".compose";
5646
+ if (view.inputState.compositionFirstChange) {
5647
+ userEvent += ".start";
5648
+ view.inputState.compositionFirstChange = false;
5649
+ }
5650
+ }
5651
+ view.dispatch(tr, { scrollIntoView: true, userEvent });
5652
+ return true;
5653
+ }
5654
+ else if (newSel && !newSel.main.eq(sel)) {
5655
+ let scrollIntoView = false, userEvent = "select";
5656
+ if (view.inputState.lastSelectionTime > Date.now() - 50) {
5657
+ if (view.inputState.lastSelectionOrigin == "select")
5658
+ scrollIntoView = true;
5659
+ userEvent = view.inputState.lastSelectionOrigin;
5660
+ }
5661
+ view.dispatch({ selection: newSel, scrollIntoView, userEvent });
5662
+ return true;
5663
+ }
5664
+ else {
5665
+ return false;
5666
+ }
5667
+ }
5668
+ function findDiff(a, b, preferredPos, preferredSide) {
5669
+ let minLen = Math.min(a.length, b.length);
5670
+ let from = 0;
5671
+ while (from < minLen && a.charCodeAt(from) == b.charCodeAt(from))
5672
+ from++;
5673
+ if (from == minLen && a.length == b.length)
5674
+ return null;
5675
+ let toA = a.length, toB = b.length;
5676
+ while (toA > 0 && toB > 0 && a.charCodeAt(toA - 1) == b.charCodeAt(toB - 1)) {
5677
+ toA--;
5678
+ toB--;
5679
+ }
5680
+ if (preferredSide == "end") {
5681
+ let adjust = Math.max(0, from - Math.min(toA, toB));
5682
+ preferredPos -= toA + adjust - from;
5683
+ }
5684
+ if (toA < from && a.length < b.length) {
5685
+ let move = preferredPos <= from && preferredPos >= toA ? from - preferredPos : 0;
5686
+ from -= move;
5687
+ toB = from + (toB - toA);
5688
+ toA = from;
5689
+ }
5690
+ else if (toB < from) {
5691
+ let move = preferredPos <= from && preferredPos >= toB ? from - preferredPos : 0;
5692
+ from -= move;
5693
+ toA = from + (toA - toB);
5694
+ toB = from;
5695
+ }
5696
+ return { from, toA, toB };
5697
+ }
5698
+ function selectionPoints(view) {
5699
+ let result = [];
5700
+ if (view.root.activeElement != view.contentDOM)
5701
+ return result;
5702
+ let { anchorNode, anchorOffset, focusNode, focusOffset } = view.observer.selectionRange;
5703
+ if (anchorNode) {
5704
+ result.push(new DOMPoint(anchorNode, anchorOffset));
5705
+ if (focusNode != anchorNode || focusOffset != anchorOffset)
5706
+ result.push(new DOMPoint(focusNode, focusOffset));
5707
+ }
5708
+ return result;
5709
+ }
5710
+ function selectionFromPoints(points, base) {
5711
+ if (points.length == 0)
5712
+ return null;
5713
+ let anchor = points[0].pos, head = points.length == 2 ? points[1].pos : anchor;
5714
+ return anchor > -1 && head > -1 ? state.EditorSelection.single(anchor + base, head + base) : null;
5715
+ }
5716
+
5433
5717
  const observeOptions = {
5434
5718
  childList: true,
5435
5719
  characterData: true,
@@ -5441,10 +5725,8 @@ const observeOptions = {
5441
5725
  // DOMCharacterDataModified there
5442
5726
  const useCharData = browser.ie && browser.ie_version <= 11;
5443
5727
  class DOMObserver {
5444
- constructor(view, onChange, onScrollChanged) {
5728
+ constructor(view) {
5445
5729
  this.view = view;
5446
- this.onChange = onChange;
5447
- this.onScrollChanged = onScrollChanged;
5448
5730
  this.active = false;
5449
5731
  // The known selection. Kept in our own object, as opposed to just
5450
5732
  // directly accessing the selection because:
@@ -5459,6 +5741,7 @@ class DOMObserver {
5459
5741
  this.resizeTimeout = -1;
5460
5742
  this.queue = [];
5461
5743
  this.delayedAndroidKey = null;
5744
+ this.flushingAndroidKey = -1;
5462
5745
  this.lastChange = 0;
5463
5746
  this.scrollTargets = [];
5464
5747
  this.intersection = null;
@@ -5527,6 +5810,11 @@ class DOMObserver {
5527
5810
  this.listenForScroll();
5528
5811
  this.readSelectionRange();
5529
5812
  }
5813
+ onScrollChanged(e) {
5814
+ this.view.inputState.runScrollHandlers(this.view, e);
5815
+ if (this.intersecting)
5816
+ this.view.measure();
5817
+ }
5530
5818
  onScroll(e) {
5531
5819
  if (this.intersecting)
5532
5820
  this.flush(false);
@@ -5686,14 +5974,17 @@ class DOMObserver {
5686
5974
  // them or, if that has no effect, dispatches the given key.
5687
5975
  delayAndroidKey(key, keyCode) {
5688
5976
  var _a;
5689
- if (!this.delayedAndroidKey)
5690
- this.view.win.requestAnimationFrame(() => {
5977
+ if (!this.delayedAndroidKey) {
5978
+ let flush = () => {
5691
5979
  let key = this.delayedAndroidKey;
5692
- this.delayedAndroidKey = null;
5693
- this.delayedFlush = -1;
5694
- if (!this.flush() && key.force)
5695
- dispatchKey(this.dom, key.key, key.keyCode);
5696
- });
5980
+ if (key) {
5981
+ this.clearDelayedAndroidKey();
5982
+ if (!this.flush() && key.force)
5983
+ dispatchKey(this.dom, key.key, key.keyCode);
5984
+ }
5985
+ };
5986
+ this.flushingAndroidKey = this.view.win.requestAnimationFrame(flush);
5987
+ }
5697
5988
  // Since backspace beforeinput is sometimes signalled spuriously,
5698
5989
  // Enter always takes precedence.
5699
5990
  if (!this.delayedAndroidKey || key == "Enter")
@@ -5706,6 +5997,11 @@ class DOMObserver {
5706
5997
  force: this.lastChange < Date.now() - 50 || !!((_a = this.delayedAndroidKey) === null || _a === void 0 ? void 0 : _a.force)
5707
5998
  };
5708
5999
  }
6000
+ clearDelayedAndroidKey() {
6001
+ this.win.cancelAnimationFrame(this.flushingAndroidKey);
6002
+ this.delayedAndroidKey = null;
6003
+ this.flushingAndroidKey = -1;
6004
+ }
5709
6005
  flushSoon() {
5710
6006
  if (this.delayedFlush < 0)
5711
6007
  this.delayedFlush = this.view.win.requestAnimationFrame(() => { this.delayedFlush = -1; this.flush(); });
@@ -5740,6 +6036,17 @@ class DOMObserver {
5740
6036
  }
5741
6037
  return { from, to, typeOver };
5742
6038
  }
6039
+ readChange() {
6040
+ let { from, to, typeOver } = this.processRecords();
6041
+ let newSel = this.selectionChanged && hasSelection(this.dom, this.selectionRange);
6042
+ if (from < 0 && !newSel)
6043
+ return null;
6044
+ if (from > -1)
6045
+ this.lastChange = Date.now();
6046
+ this.view.inputState.lastFocusTime = 0;
6047
+ this.selectionChanged = false;
6048
+ return new DOMChange(this.view, from, to, typeOver);
6049
+ }
5743
6050
  // Apply pending changes, if any
5744
6051
  flush(readSelection = true) {
5745
6052
  // Completely hold off flushing when pending keys are set—the code
@@ -5749,16 +6056,11 @@ class DOMObserver {
5749
6056
  return false;
5750
6057
  if (readSelection)
5751
6058
  this.readSelectionRange();
5752
- let { from, to, typeOver } = this.processRecords();
5753
- let newSel = this.selectionChanged && hasSelection(this.dom, this.selectionRange);
5754
- if (from < 0 && !newSel)
6059
+ let domChange = this.readChange();
6060
+ if (!domChange)
5755
6061
  return false;
5756
- if (from > -1)
5757
- this.lastChange = Date.now();
5758
- this.view.inputState.lastFocusTime = 0;
5759
- this.selectionChanged = false;
5760
6062
  let startState = this.view.state;
5761
- let handled = this.onChange(from, to, typeOver);
6063
+ let handled = applyDOMChange(this.view, domChange);
5762
6064
  // The view wasn't updated
5763
6065
  if (this.view.state == startState)
5764
6066
  this.view.update([]);
@@ -5814,6 +6116,8 @@ class DOMObserver {
5814
6116
  this.removeWindowListeners(this.win);
5815
6117
  clearTimeout(this.parentCheck);
5816
6118
  clearTimeout(this.resizeTimeout);
6119
+ this.win.cancelAnimationFrame(this.delayedFlush);
6120
+ this.win.cancelAnimationFrame(this.flushingAndroidKey);
5817
6121
  }
5818
6122
  }
5819
6123
  function findChild(cView, dom, dir) {
@@ -5855,213 +6159,6 @@ function safariSelectionRangeHack(view) {
5855
6159
  return { anchorNode, anchorOffset, focusNode, focusOffset };
5856
6160
  }
5857
6161
 
5858
- function applyDOMChange(view, start, end, typeOver) {
5859
- let change, newSel;
5860
- let sel = view.state.selection.main;
5861
- if (start > -1) {
5862
- let bounds = view.docView.domBoundsAround(start, end, 0);
5863
- if (!bounds || view.state.readOnly)
5864
- return false;
5865
- let { from, to } = bounds;
5866
- let selPoints = view.docView.impreciseHead || view.docView.impreciseAnchor ? [] : selectionPoints(view);
5867
- let reader = new DOMReader(selPoints, view.state);
5868
- reader.readRange(bounds.startDOM, bounds.endDOM);
5869
- let preferredPos = sel.from, preferredSide = null;
5870
- // Prefer anchoring to end when Backspace is pressed (or, on
5871
- // Android, when something was deleted)
5872
- if (view.inputState.lastKeyCode === 8 && view.inputState.lastKeyTime > Date.now() - 100 ||
5873
- browser.android && reader.text.length < to - from) {
5874
- preferredPos = sel.to;
5875
- preferredSide = "end";
5876
- }
5877
- let diff = findDiff(view.state.doc.sliceString(from, to, LineBreakPlaceholder), reader.text, preferredPos - from, preferredSide);
5878
- if (diff) {
5879
- // Chrome inserts two newlines when pressing shift-enter at the
5880
- // end of a line. This drops one of those.
5881
- if (browser.chrome && view.inputState.lastKeyCode == 13 &&
5882
- diff.toB == diff.from + 2 && reader.text.slice(diff.from, diff.toB) == LineBreakPlaceholder + LineBreakPlaceholder)
5883
- diff.toB--;
5884
- change = { from: from + diff.from, to: from + diff.toA,
5885
- insert: state.Text.of(reader.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
5886
- }
5887
- newSel = selectionFromPoints(selPoints, from);
5888
- }
5889
- else if (view.hasFocus || !view.state.facet(editable)) {
5890
- let domSel = view.observer.selectionRange;
5891
- let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
5892
- let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ||
5893
- !contains(view.contentDOM, domSel.focusNode)
5894
- ? view.state.selection.main.head
5895
- : view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
5896
- let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset ||
5897
- !contains(view.contentDOM, domSel.anchorNode)
5898
- ? view.state.selection.main.anchor
5899
- : view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
5900
- if (head != sel.head || anchor != sel.anchor)
5901
- newSel = state.EditorSelection.single(anchor, head);
5902
- }
5903
- if (!change && !newSel)
5904
- return false;
5905
- // Heuristic to notice typing over a selected character
5906
- if (!change && typeOver && !sel.empty && newSel && newSel.main.empty)
5907
- change = { from: sel.from, to: sel.to, insert: view.state.doc.slice(sel.from, sel.to) };
5908
- // If the change is inside the selection and covers most of it,
5909
- // assume it is a selection replace (with identical characters at
5910
- // the start/end not included in the diff)
5911
- else if (change && change.from >= sel.from && change.to <= sel.to &&
5912
- (change.from != sel.from || change.to != sel.to) &&
5913
- (sel.to - sel.from) - (change.to - change.from) <= 4)
5914
- change = {
5915
- from: sel.from, to: sel.to,
5916
- insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
5917
- };
5918
- // Detect insert-period-on-double-space Mac behavior, and transform
5919
- // it into a regular space insert.
5920
- else if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
5921
- change.insert.toString() == ".")
5922
- change = { from: sel.from, to: sel.to, insert: state.Text.of([" "]) };
5923
- if (change) {
5924
- let startState = view.state;
5925
- if (browser.ios && view.inputState.flushIOSKey(view))
5926
- return true;
5927
- // Android browsers don't fire reasonable key events for enter,
5928
- // backspace, or delete. So this detects changes that look like
5929
- // they're caused by those keys, and reinterprets them as key
5930
- // events. (Some of these keys are also handled by beforeinput
5931
- // events and the pendingAndroidKey mechanism, but that's not
5932
- // reliable in all situations.)
5933
- if (browser.android &&
5934
- ((change.from == sel.from && change.to == sel.to &&
5935
- change.insert.length == 1 && change.insert.lines == 2 &&
5936
- dispatchKey(view.contentDOM, "Enter", 13)) ||
5937
- (change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 &&
5938
- dispatchKey(view.contentDOM, "Backspace", 8)) ||
5939
- (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
5940
- dispatchKey(view.contentDOM, "Delete", 46))))
5941
- return true;
5942
- let text = change.insert.toString();
5943
- if (view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text)))
5944
- return true;
5945
- if (view.inputState.composing >= 0)
5946
- view.inputState.composing++;
5947
- let tr;
5948
- if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
5949
- (!newSel || newSel.main.empty && newSel.main.from == change.from + change.insert.length) &&
5950
- view.inputState.composing < 0) {
5951
- let before = sel.from < change.from ? startState.sliceDoc(sel.from, change.from) : "";
5952
- let after = sel.to > change.to ? startState.sliceDoc(change.to, sel.to) : "";
5953
- tr = startState.replaceSelection(view.state.toText(before + change.insert.sliceString(0, undefined, view.state.lineBreak) + after));
5954
- }
5955
- else {
5956
- let changes = startState.changes(change);
5957
- let mainSel = newSel && !startState.selection.main.eq(newSel.main) && newSel.main.to <= changes.newLength
5958
- ? newSel.main : undefined;
5959
- // Try to apply a composition change to all cursors
5960
- if (startState.selection.ranges.length > 1 && view.inputState.composing >= 0 &&
5961
- change.to <= sel.to && change.to >= sel.to - 10) {
5962
- let replaced = view.state.sliceDoc(change.from, change.to);
5963
- let compositionRange = compositionSurroundingNode(view) || view.state.doc.lineAt(sel.head);
5964
- let offset = sel.to - change.to, size = sel.to - sel.from;
5965
- tr = startState.changeByRange(range => {
5966
- if (range.from == sel.from && range.to == sel.to)
5967
- return { changes, range: mainSel || range.map(changes) };
5968
- let to = range.to - offset, from = to - replaced.length;
5969
- if (range.to - range.from != size || view.state.sliceDoc(from, to) != replaced ||
5970
- // Unfortunately, there's no way to make multiple
5971
- // changes in the same node work without aborting
5972
- // composition, so cursors in the composition range are
5973
- // ignored.
5974
- compositionRange && range.to >= compositionRange.from && range.from <= compositionRange.to)
5975
- return { range };
5976
- let rangeChanges = startState.changes({ from, to, insert: change.insert }), selOff = range.to - sel.to;
5977
- return {
5978
- changes: rangeChanges,
5979
- range: !mainSel ? range.map(rangeChanges) :
5980
- state.EditorSelection.range(Math.max(0, mainSel.anchor + selOff), Math.max(0, mainSel.head + selOff))
5981
- };
5982
- });
5983
- }
5984
- else {
5985
- tr = {
5986
- changes,
5987
- selection: mainSel && startState.selection.replaceRange(mainSel)
5988
- };
5989
- }
5990
- }
5991
- let userEvent = "input.type";
5992
- if (view.composing) {
5993
- userEvent += ".compose";
5994
- if (view.inputState.compositionFirstChange) {
5995
- userEvent += ".start";
5996
- view.inputState.compositionFirstChange = false;
5997
- }
5998
- }
5999
- view.dispatch(tr, { scrollIntoView: true, userEvent });
6000
- return true;
6001
- }
6002
- else if (newSel && !newSel.main.eq(sel)) {
6003
- let scrollIntoView = false, userEvent = "select";
6004
- if (view.inputState.lastSelectionTime > Date.now() - 50) {
6005
- if (view.inputState.lastSelectionOrigin == "select")
6006
- scrollIntoView = true;
6007
- userEvent = view.inputState.lastSelectionOrigin;
6008
- }
6009
- view.dispatch({ selection: newSel, scrollIntoView, userEvent });
6010
- return true;
6011
- }
6012
- else {
6013
- return false;
6014
- }
6015
- }
6016
- function findDiff(a, b, preferredPos, preferredSide) {
6017
- let minLen = Math.min(a.length, b.length);
6018
- let from = 0;
6019
- while (from < minLen && a.charCodeAt(from) == b.charCodeAt(from))
6020
- from++;
6021
- if (from == minLen && a.length == b.length)
6022
- return null;
6023
- let toA = a.length, toB = b.length;
6024
- while (toA > 0 && toB > 0 && a.charCodeAt(toA - 1) == b.charCodeAt(toB - 1)) {
6025
- toA--;
6026
- toB--;
6027
- }
6028
- if (preferredSide == "end") {
6029
- let adjust = Math.max(0, from - Math.min(toA, toB));
6030
- preferredPos -= toA + adjust - from;
6031
- }
6032
- if (toA < from && a.length < b.length) {
6033
- let move = preferredPos <= from && preferredPos >= toA ? from - preferredPos : 0;
6034
- from -= move;
6035
- toB = from + (toB - toA);
6036
- toA = from;
6037
- }
6038
- else if (toB < from) {
6039
- let move = preferredPos <= from && preferredPos >= toB ? from - preferredPos : 0;
6040
- from -= move;
6041
- toA = from + (toA - toB);
6042
- toB = from;
6043
- }
6044
- return { from, toA, toB };
6045
- }
6046
- function selectionPoints(view) {
6047
- let result = [];
6048
- if (view.root.activeElement != view.contentDOM)
6049
- return result;
6050
- let { anchorNode, anchorOffset, focusNode, focusOffset } = view.observer.selectionRange;
6051
- if (anchorNode) {
6052
- result.push(new DOMPoint(anchorNode, anchorOffset));
6053
- if (focusNode != anchorNode || focusOffset != anchorOffset)
6054
- result.push(new DOMPoint(focusNode, focusOffset));
6055
- }
6056
- return result;
6057
- }
6058
- function selectionFromPoints(points, base) {
6059
- if (points.length == 0)
6060
- return null;
6061
- let anchor = points[0].pos, head = points.length == 2 ? points[1].pos : anchor;
6062
- return anchor > -1 && head > -1 ? state.EditorSelection.single(anchor + base, head + base) : null;
6063
- }
6064
-
6065
6162
  // The editor's update state machine looks something like this:
6066
6163
  //
6067
6164
  // Idle → Updating ⇆ Idle (unchecked) → Measuring → Idle
@@ -6124,13 +6221,7 @@ class EditorView {
6124
6221
  this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec));
6125
6222
  for (let plugin of this.plugins)
6126
6223
  plugin.update(this);
6127
- this.observer = new DOMObserver(this, (from, to, typeOver) => {
6128
- return applyDOMChange(this, from, to, typeOver);
6129
- }, event => {
6130
- this.inputState.runScrollHandlers(this, event);
6131
- if (this.observer.intersecting)
6132
- this.measure();
6133
- });
6224
+ this.observer = new DOMObserver(this);
6134
6225
  this.inputState = new InputState(this);
6135
6226
  this.inputState.ensureHandlers(this, this.plugins);
6136
6227
  this.docView = new DocView(this);
@@ -6214,7 +6305,20 @@ class EditorView {
6214
6305
  this.viewState.state = state$1;
6215
6306
  return;
6216
6307
  }
6217
- this.observer.clear();
6308
+ // If there was a pending DOM change, eagerly read it and try to
6309
+ // apply it after the given transactions.
6310
+ let pendingKey = this.observer.delayedAndroidKey, domChange = null;
6311
+ if (pendingKey) {
6312
+ this.observer.clearDelayedAndroidKey();
6313
+ domChange = this.observer.readChange();
6314
+ // Only try to apply DOM changes if the transactions didn't
6315
+ // change the doc or selection.
6316
+ if (domChange && !this.state.doc.eq(state$1.doc) || !this.state.selection.eq(state$1.selection))
6317
+ domChange = null;
6318
+ }
6319
+ else {
6320
+ this.observer.clear();
6321
+ }
6218
6322
  // When the phrases change, redraw the editor
6219
6323
  if (state$1.facet(state.EditorState.phrases) != this.state.facet(state.EditorState.phrases))
6220
6324
  return this.setState(state$1);
@@ -6256,6 +6360,10 @@ class EditorView {
6256
6360
  if (!update.empty)
6257
6361
  for (let listener of this.state.facet(updateListener))
6258
6362
  listener(update);
6363
+ if (domChange) {
6364
+ if (!applyDOMChange(this, domChange) && pendingKey.force)
6365
+ dispatchKey(this.contentDOM, pendingKey.key, pendingKey.keyCode);
6366
+ }
6259
6367
  }
6260
6368
  /**
6261
6369
  Reset the view to the given state. (This will cause the entire
@@ -7093,6 +7201,7 @@ function buildKeymap(bindings, platform = currentPlatform) {
7093
7201
  throw new Error("Key binding " + name + " is used both as a regular binding and as a multi-stroke prefix");
7094
7202
  };
7095
7203
  let add = (scope, key, command, preventDefault) => {
7204
+ var _a, _b;
7096
7205
  let scopeObj = bound[scope] || (bound[scope] = Object.create(null));
7097
7206
  let parts = key.split(/ (?!$)/).map(k => normalizeKeyName(k, platform));
7098
7207
  for (let i = 1; i < parts.length; i++) {
@@ -7101,7 +7210,7 @@ function buildKeymap(bindings, platform = currentPlatform) {
7101
7210
  if (!scopeObj[prefix])
7102
7211
  scopeObj[prefix] = {
7103
7212
  preventDefault: true,
7104
- commands: [(view) => {
7213
+ run: [(view) => {
7105
7214
  let ourObj = storedPrefix = { view, prefix, scope };
7106
7215
  setTimeout(() => { if (storedPrefix == ourObj)
7107
7216
  storedPrefix = null; }, PrefixTimeout);
@@ -7111,16 +7220,26 @@ function buildKeymap(bindings, platform = currentPlatform) {
7111
7220
  }
7112
7221
  let full = parts.join(" ");
7113
7222
  checkPrefix(full, false);
7114
- let binding = scopeObj[full] || (scopeObj[full] = { preventDefault: false, commands: [] });
7115
- binding.commands.push(command);
7223
+ let binding = scopeObj[full] || (scopeObj[full] = { preventDefault: false, run: ((_b = (_a = scopeObj._any) === null || _a === void 0 ? void 0 : _a.run) === null || _b === void 0 ? void 0 : _b.slice()) || [] });
7224
+ if (command)
7225
+ binding.run.push(command);
7116
7226
  if (preventDefault)
7117
7227
  binding.preventDefault = true;
7118
7228
  };
7119
7229
  for (let b of bindings) {
7230
+ let scopes = b.scope ? b.scope.split(" ") : ["editor"];
7231
+ if (b.any)
7232
+ for (let scope of scopes) {
7233
+ let scopeObj = bound[scope] || (bound[scope] = Object.create(null));
7234
+ if (!scopeObj._any)
7235
+ scopeObj._any = { preventDefault: false, run: [] };
7236
+ for (let key in scopeObj)
7237
+ scopeObj[key].run.push(b.any);
7238
+ }
7120
7239
  let name = b[platform] || b.key;
7121
7240
  if (!name)
7122
7241
  continue;
7123
- for (let scope of b.scope ? b.scope.split(" ") : ["editor"]) {
7242
+ for (let scope of scopes) {
7124
7243
  add(scope, name, b.run, b.preventDefault);
7125
7244
  if (b.shift)
7126
7245
  add(scope, "Shift-" + name, b.shift, b.preventDefault);
@@ -7137,11 +7256,15 @@ function runHandlers(map, event, view, scope) {
7137
7256
  if (fallthrough = modifierCodes.indexOf(event.keyCode) < 0)
7138
7257
  storedPrefix = null;
7139
7258
  }
7259
+ let ran = new Set;
7140
7260
  let runFor = (binding) => {
7141
7261
  if (binding) {
7142
- for (let cmd of binding.commands)
7143
- if (cmd(view))
7144
- return true;
7262
+ for (let cmd of binding.run)
7263
+ if (!ran.has(cmd)) {
7264
+ ran.add(cmd);
7265
+ if (cmd(view, event))
7266
+ return true;
7267
+ }
7145
7268
  if (binding.preventDefault)
7146
7269
  fallthrough = true;
7147
7270
  }
@@ -7163,6 +7286,8 @@ function runHandlers(map, event, view, scope) {
7163
7286
  if (runFor(scopeObj[prefix + modifiers(name, event, true)]))
7164
7287
  return true;
7165
7288
  }
7289
+ if (runFor(scopeObj._any))
7290
+ return true;
7166
7291
  }
7167
7292
  return fallthrough;
7168
7293
  }