@codemirror/view 0.19.9 → 0.19.10

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,27 @@
1
+ ## 0.19.10 (2021-11-02)
2
+
3
+ ### Bug fixes
4
+
5
+ Don't crash when `IntersectionObserver` fires its callback without any records. Try to handle some backspace issues on Chrome Android
6
+
7
+ Using backspace near uneditable widgets on Chrome Android should now be more reliable.
8
+
9
+ Work around a number of browser bugs by always rendering zero-width spaces around in-content widgets, so that browsers will treat the positions near them as valid cursor positions and not try to run composition across widget boundaries.
10
+
11
+ Work around bogus composition changes created by Chrome Android after handled backspace presses.
12
+
13
+ Work around an issue where tapping on an uneditable node in the editor would sometimes fail to show the virtual keyboard on Chrome Android.
14
+
15
+ Prevent translation services from translating the editor content. Show direction override characters as special chars by default
16
+
17
+ `specialChars` will now, by default, replace direction override chars, to mitigate https://trojansource.codes/ attacks.
18
+
19
+ ### New features
20
+
21
+ The editor view will, if `parent` is given but `root` is not, derive the root from the parent element.
22
+
23
+ Line decorations now accept a `class` property to directly add DOM classes to the line.
24
+
1
25
  ## 0.19.9 (2021-10-01)
2
26
 
3
27
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -251,6 +251,14 @@ function contentEditablePlainTextSupported() {
251
251
  }
252
252
  return _plainTextSupported;
253
253
  }
254
+ function getRoot(node) {
255
+ while (node) {
256
+ node = node.assignedSlot || node.parentNode;
257
+ if (node && (node.nodeType == 9 || node.nodeType == 11 && node.host))
258
+ return node;
259
+ }
260
+ return null;
261
+ }
254
262
 
255
263
  class DOMPos {
256
264
  constructor(node, offset, precise = true) {
@@ -448,6 +456,7 @@ class ContentView {
448
456
  (this.breakAfter ? "#" : "");
449
457
  }
450
458
  static get(node) { return node.cmView; }
459
+ get isEditable() { return true; }
451
460
  }
452
461
  ContentView.prototype.breakAfter = 0;
453
462
  // Remove a DOM node and return its next sibling.
@@ -495,15 +504,18 @@ const gecko = !ie && /gecko\/(\d+)/i.test(nav.userAgent);
495
504
  const chrome = !ie && /Chrome\/(\d+)/.exec(nav.userAgent);
496
505
  const webkit = "webkitFontSmoothing" in doc.documentElement.style;
497
506
  const safari = !ie && /Apple Computer/.test(nav.vendor);
507
+ const ios = safari && (/Mobile\/\w+/.test(nav.userAgent) || nav.maxTouchPoints > 2);
498
508
  var browser = {
499
- mac: /Mac/.test(nav.platform),
509
+ mac: ios || /Mac/.test(nav.platform),
510
+ windows: /Win/.test(nav.platform),
511
+ linux: /Linux|X11/.test(nav.platform),
500
512
  ie,
501
513
  ie_version: ie_upto10 ? doc.documentMode || 6 : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0,
502
514
  gecko,
503
515
  gecko_version: gecko ? +(/Firefox\/(\d+)/.exec(nav.userAgent) || [0, 0])[1] : 0,
504
516
  chrome: !!chrome,
505
517
  chrome_version: chrome ? +chrome[1] : 0,
506
- ios: safari && (/Mobile\/\w+/.test(nav.userAgent) || nav.maxTouchPoints > 2),
518
+ ios,
507
519
  android: /Android\b/.test(nav.userAgent),
508
520
  webkit,
509
521
  safari,
@@ -725,6 +737,7 @@ class WidgetView extends InlineView {
725
737
  }
726
738
  return (pos == 0 && side > 0 || pos == this.length && side <= 0) ? rect : flattenRect(rect, pos == 0);
727
739
  }
740
+ get isEditable() { return false; }
728
741
  }
729
742
  class CompositionView extends WidgetView {
730
743
  domAtPos(pos) { return new DOMPos(this.widget.text, pos); }
@@ -736,6 +749,38 @@ class CompositionView extends WidgetView {
736
749
  ignoreMutation() { return false; }
737
750
  get overrideDOMText() { return null; }
738
751
  coordsAt(pos, side) { return textCoords(this.widget.text, pos, side); }
752
+ get isEditable() { return true; }
753
+ }
754
+ // These are drawn around uneditable widgets to avoid a number of
755
+ // browser bugs that show up when the cursor is directly next to
756
+ // uneditable inline content.
757
+ class WidgetBufferView extends InlineView {
758
+ constructor(side) {
759
+ super();
760
+ this.side = side;
761
+ }
762
+ get length() { return 0; }
763
+ merge() { return false; }
764
+ become(other) {
765
+ return other instanceof WidgetBufferView && other.side == this.side;
766
+ }
767
+ slice() { return new WidgetBufferView(this.side); }
768
+ sync() {
769
+ if (!this.dom)
770
+ this.setDOM(document.createTextNode("\u200b"));
771
+ else if (this.dirty && this.dom.nodeValue != "\u200b")
772
+ this.dom.nodeValue = "\u200b";
773
+ }
774
+ getSide() { return this.side; }
775
+ domAtPos(pos) { return DOMPos.before(this.dom); }
776
+ domBoundsAround() { return null; }
777
+ coordsAt(pos) {
778
+ let rects = clientRectsFor(this.dom);
779
+ return rects[rects.length - 1];
780
+ }
781
+ get overrideDOMText() {
782
+ return text.Text.of([this.dom.nodeValue.replace(/\u200b/g, "")]);
783
+ }
739
784
  }
740
785
  function mergeInlineChildren(parent, from, to, elts, openStart, openEnd) {
741
786
  let cur = parent.childCursor();
@@ -1231,14 +1276,17 @@ class LineView extends ContentView {
1231
1276
  }
1232
1277
  // Only called when building a line view in ContentBuilder
1233
1278
  addLineDeco(deco) {
1234
- let attrs = deco.spec.attributes;
1279
+ let attrs = deco.spec.attributes, cls = deco.spec.class;
1235
1280
  if (attrs)
1236
1281
  this.attrs = combineAttrs(attrs, this.attrs || {});
1282
+ if (cls)
1283
+ this.attrs = combineAttrs(attrs, { class: cls });
1237
1284
  }
1238
1285
  domAtPos(pos) {
1239
1286
  return inlineDOMAtPos(this.dom, this.children, pos);
1240
1287
  }
1241
1288
  sync(track) {
1289
+ var _a;
1242
1290
  if (!this.dom || (this.dirty & 4 /* Attrs */)) {
1243
1291
  this.setDOM(document.createElement("div"));
1244
1292
  this.dom.className = "cm-line";
@@ -1254,7 +1302,7 @@ class LineView extends ContentView {
1254
1302
  while (last && ContentView.get(last) instanceof MarkView)
1255
1303
  last = last.lastChild;
1256
1304
  if (!last ||
1257
- last.nodeName != "BR" && ContentView.get(last) instanceof WidgetView &&
1305
+ last.nodeName != "BR" && ((_a = ContentView.get(last)) === null || _a === void 0 ? void 0 : _a.isEditable) == false &&
1258
1306
  (!browser.ios || !this.children.some(ch => ch instanceof TextView))) {
1259
1307
  let hack = document.createElement("BR");
1260
1308
  hack.cmIgnore = true;
@@ -1353,6 +1401,9 @@ class ContentBuilder {
1353
1401
  this.content = [];
1354
1402
  this.curLine = null;
1355
1403
  this.breakAtStart = 0;
1404
+ this.pendingBuffer = 0 /* No */;
1405
+ // Set to false directly after a widget that covers the position after it
1406
+ this.atCursorPos = true;
1356
1407
  this.openStart = -1;
1357
1408
  this.openEnd = -1;
1358
1409
  this.text = "";
@@ -1367,23 +1418,31 @@ class ContentBuilder {
1367
1418
  return !last.breakAfter && !(last instanceof BlockWidgetView && last.type == exports.BlockType.WidgetBefore);
1368
1419
  }
1369
1420
  getLine() {
1370
- if (!this.curLine)
1421
+ if (!this.curLine) {
1371
1422
  this.content.push(this.curLine = new LineView);
1423
+ this.atCursorPos = true;
1424
+ }
1372
1425
  return this.curLine;
1373
1426
  }
1374
- addWidget(view) {
1427
+ flushBuffer(active) {
1428
+ if (this.pendingBuffer) {
1429
+ this.curLine.append(wrapMarks(new WidgetBufferView(-1), active), active.length);
1430
+ this.pendingBuffer = 0 /* No */;
1431
+ }
1432
+ }
1433
+ addBlockWidget(view) {
1434
+ this.flushBuffer([]);
1375
1435
  this.curLine = null;
1376
1436
  this.content.push(view);
1377
1437
  }
1378
- finish() {
1438
+ finish(openEnd) {
1439
+ if (!openEnd)
1440
+ this.flushBuffer([]);
1441
+ else
1442
+ this.pendingBuffer = 0 /* No */;
1379
1443
  if (!this.posCovered())
1380
1444
  this.getLine();
1381
1445
  }
1382
- wrapMarks(view, active) {
1383
- for (let mark of active)
1384
- view = new MarkView(mark, [view], view.length);
1385
- return view;
1386
- }
1387
1446
  buildText(length, active, openStart) {
1388
1447
  while (length > 0) {
1389
1448
  if (this.textOff == this.text.length) {
@@ -1398,6 +1457,7 @@ class ContentBuilder {
1398
1457
  this.content[this.content.length - 1].breakAfter = 1;
1399
1458
  else
1400
1459
  this.breakAtStart = 1;
1460
+ this.flushBuffer([]);
1401
1461
  this.curLine = null;
1402
1462
  length--;
1403
1463
  continue;
@@ -1408,7 +1468,9 @@ class ContentBuilder {
1408
1468
  }
1409
1469
  }
1410
1470
  let take = Math.min(this.text.length - this.textOff, length, 512 /* Chunk */);
1411
- this.getLine().append(this.wrapMarks(new TextView(this.text.slice(this.textOff, this.textOff + take)), active), openStart);
1471
+ this.flushBuffer(active);
1472
+ this.getLine().append(wrapMarks(new TextView(this.text.slice(this.textOff, this.textOff + take)), active), openStart);
1473
+ this.atCursorPos = true;
1412
1474
  this.textOff += take;
1413
1475
  length -= take;
1414
1476
  openStart = 0;
@@ -1427,11 +1489,23 @@ class ContentBuilder {
1427
1489
  let { type } = deco;
1428
1490
  if (type == exports.BlockType.WidgetAfter && !this.posCovered())
1429
1491
  this.getLine();
1430
- this.addWidget(new BlockWidgetView(deco.widget || new NullWidget("div"), len, type));
1492
+ this.addBlockWidget(new BlockWidgetView(deco.widget || new NullWidget("div"), len, type));
1431
1493
  }
1432
1494
  else {
1433
- let widget = this.wrapMarks(WidgetView.create(deco.widget || new NullWidget("span"), len, deco.startSide), active);
1434
- this.getLine().append(widget, openStart);
1495
+ let view = WidgetView.create(deco.widget || new NullWidget("span"), len, deco.startSide);
1496
+ let cursorBefore = this.atCursorPos && !view.isEditable && openStart <= active.length && (from < to || deco.startSide > 0);
1497
+ let cursorAfter = !view.isEditable && (from < to || deco.startSide <= 0);
1498
+ let line = this.getLine();
1499
+ if (this.pendingBuffer == 2 /* IfCursor */ && !cursorBefore)
1500
+ this.pendingBuffer = 0 /* No */;
1501
+ this.flushBuffer(active);
1502
+ if (cursorBefore) {
1503
+ line.append(wrapMarks(new WidgetBufferView(1), active), openStart);
1504
+ openStart = active.length + Math.max(0, openStart - active.length);
1505
+ }
1506
+ line.append(wrapMarks(view, active), openStart);
1507
+ this.atCursorPos = cursorAfter;
1508
+ this.pendingBuffer = !cursorAfter ? 0 /* No */ : from < to ? 1 /* Yes */ : 2 /* IfCursor */;
1435
1509
  }
1436
1510
  }
1437
1511
  else if (this.doc.lineAt(this.pos).from == this.pos) { // Line decoration
@@ -1457,10 +1531,15 @@ class ContentBuilder {
1457
1531
  builder.openEnd = rangeset.RangeSet.spans(decorations, from, to, builder);
1458
1532
  if (builder.openStart < 0)
1459
1533
  builder.openStart = builder.openEnd;
1460
- builder.finish();
1534
+ builder.finish(builder.openEnd);
1461
1535
  return builder;
1462
1536
  }
1463
1537
  }
1538
+ function wrapMarks(view, active) {
1539
+ for (let mark of active)
1540
+ view = new MarkView(mark, [view], view.length);
1541
+ return view;
1542
+ }
1464
1543
  class NullWidget extends WidgetType {
1465
1544
  constructor(tag) {
1466
1545
  super();
@@ -1907,6 +1986,14 @@ class DocView extends ContentView {
1907
1986
  return true;
1908
1987
  }
1909
1988
  }
1989
+ reset(sel) {
1990
+ if (this.dirty) {
1991
+ this.view.observer.ignore(() => this.view.docView.sync());
1992
+ this.dirty = 0 /* Not */;
1993
+ }
1994
+ if (sel)
1995
+ this.updateSelection();
1996
+ }
1910
1997
  // Used both by update and checkLayout do perform the actual DOM
1911
1998
  // update
1912
1999
  updateInner(changes, deco, oldLength, forceSelection = false, pointerSel = false) {
@@ -2036,6 +2123,14 @@ class DocView extends ContentView {
2036
2123
  !isEquivalentPosition(anchor.node, anchor.offset, domSel.anchorNode, domSel.anchorOffset) ||
2037
2124
  !isEquivalentPosition(head.node, head.offset, domSel.focusNode, domSel.focusOffset)) {
2038
2125
  this.view.observer.ignore(() => {
2126
+ // Chrome Android will hide the virtual keyboard when tapping
2127
+ // inside an uneditable node, and not bring it back when we
2128
+ // move the cursor to its proper position. This tries to
2129
+ // restore the keyboard by cycling focus.
2130
+ if (browser.android && browser.chrome && this.dom.contains(domSel.focusNode) && inUneditable(domSel.focusNode, this.dom)) {
2131
+ this.dom.blur();
2132
+ this.dom.focus({ preventScroll: true });
2133
+ }
2039
2134
  let rawSel = getSelection(this.root);
2040
2135
  if (main.empty) {
2041
2136
  // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=1612076
@@ -2343,6 +2438,14 @@ function findChangedDeco(a, b, diff) {
2343
2438
  rangeset.RangeSet.compare(a, b, diff, comp);
2344
2439
  return comp.changes;
2345
2440
  }
2441
+ function inUneditable(node, inside) {
2442
+ for (let cur = node; cur && cur != inside; cur = cur.assignedSlot || cur.parentNode) {
2443
+ if (cur.nodeType == 1 && cur.contentEditable == 'false') {
2444
+ return true;
2445
+ }
2446
+ }
2447
+ return false;
2448
+ }
2346
2449
 
2347
2450
  /**
2348
2451
  Used to indicate [text direction](https://codemirror.net/6/docs/ref/#view.EditorView.textDirection).
@@ -2789,6 +2892,7 @@ function domPosInText(node, x, y) {
2789
2892
  return { node, offset: closestOffset > -1 ? closestOffset : generalSide > 0 ? node.nodeValue.length : 0 };
2790
2893
  }
2791
2894
  function posAtCoords(view, { x, y }, precise, bias = -1) {
2895
+ var _a;
2792
2896
  let content = view.contentDOM.getBoundingClientRect(), block;
2793
2897
  let halfLine = view.defaultLineHeight / 2;
2794
2898
  for (let bounced = false;;) {
@@ -2819,7 +2923,7 @@ function posAtCoords(view, { x, y }, precise, bias = -1) {
2819
2923
  // There's visible editor content under the point, so we can try
2820
2924
  // using caret(Position|Range)FromPoint as a shortcut
2821
2925
  let node, offset = -1;
2822
- if (element && view.contentDOM.contains(element) && !(view.docView.nearest(element) instanceof WidgetView)) {
2926
+ if (element && view.contentDOM.contains(element) && ((_a = view.docView.nearest(element)) === null || _a === void 0 ? void 0 : _a.isEditable) != false) {
2823
2927
  if (doc.caretPositionFromPoint) {
2824
2928
  let pos = doc.caretPositionFromPoint(x, y);
2825
2929
  if (pos)
@@ -2960,7 +3064,23 @@ class InputState {
2960
3064
  constructor(view) {
2961
3065
  this.lastKeyCode = 0;
2962
3066
  this.lastKeyTime = 0;
2963
- this.pendingIOSKey = null;
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
+ // On Chrome Android, backspace near widgets is just completely
3072
+ // broken, and there are no key events, so we need to handle the
3073
+ // beforeinput event. Deleting stuff will often create a flurry of
3074
+ // events, and interrupting it before it is done just makes
3075
+ // subsequent events even more broken, so again, we hold off doing
3076
+ // anything until the browser is finished with whatever it is trying
3077
+ // 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;
2964
3084
  this.lastSelectionOrigin = null;
2965
3085
  this.lastSelectionTime = 0;
2966
3086
  this.lastEscPress = 0;
@@ -3069,20 +3189,27 @@ class InputState {
3069
3189
  // state. So we let it go through, and then, in
3070
3190
  // applyDOMChange, notify key handlers of it and reset to
3071
3191
  // the state they produce.
3072
- if (browser.ios && (event.keyCode == 13 || event.keyCode == 8) &&
3192
+ let pending;
3193
+ if (browser.ios && (pending = PendingKeys.find(key => key.keyCode == event.keyCode)) &&
3073
3194
  !(event.ctrlKey || event.altKey || event.metaKey) && !event.synthetic) {
3074
- this.pendingIOSKey = event.keyCode == 13 ? "enter" : "backspace";
3075
- setTimeout(() => this.flushIOSKey(view), 250);
3195
+ this.setPendingKey(view, pending);
3076
3196
  return true;
3077
3197
  }
3078
3198
  return false;
3079
3199
  }
3080
- flushIOSKey(view) {
3081
- if (!this.pendingIOSKey)
3082
- return false;
3083
- let dom = view.contentDOM, key = this.pendingIOSKey;
3084
- this.pendingIOSKey = null;
3085
- return key == "enter" ? dispatchKey(dom, "Enter", 13) : dispatchKey(dom, "Backspace", 8);
3200
+ setPendingKey(view, pending) {
3201
+ this.pendingKey = pending;
3202
+ requestAnimationFrame(() => {
3203
+ if (!this.pendingKey)
3204
+ return false;
3205
+ let key = this.pendingKey;
3206
+ this.pendingKey = undefined;
3207
+ view.observer.processRecords();
3208
+ let startState = view.state;
3209
+ dispatchKey(view.contentDOM, key.key, key.keyCode);
3210
+ if (view.state == startState)
3211
+ view.docView.reset(true);
3212
+ });
3086
3213
  }
3087
3214
  ignoreDuringComposition(event) {
3088
3215
  if (!/^key/.test(event.type))
@@ -3129,6 +3256,11 @@ class InputState {
3129
3256
  this.mouseSelection.destroy();
3130
3257
  }
3131
3258
  }
3259
+ const PendingKeys = [
3260
+ { key: "Backspace", keyCode: 8, inputType: "deleteContentBackward" },
3261
+ { key: "Enter", keyCode: 13, inputType: "insertParagraph" },
3262
+ { key: "Delete", keyCode: 46, inputType: "deleteContentForward" }
3263
+ ];
3132
3264
  // Key codes for modifier keys
3133
3265
  const modifierCodes = [16, 17, 18, 20, 91, 92, 224, 225];
3134
3266
  class MouseSelection {
@@ -3574,6 +3706,31 @@ handlers.compositionend = view => {
3574
3706
  handlers.contextmenu = view => {
3575
3707
  view.inputState.lastContextMenu = Date.now();
3576
3708
  };
3709
+ handlers.beforeinput = (view, event) => {
3710
+ var _a;
3711
+ // Because Chrome Android doesn't fire useful key events, use
3712
+ // beforeinput to detect backspace (and possibly enter and delete,
3713
+ // but those usually don't even seem to fire beforeinput events at
3714
+ // the moment) and fake a key event for it.
3715
+ //
3716
+ // (preventDefault on beforeinput, though supported in the spec,
3717
+ // seems to do nothing at all on Chrome).
3718
+ let pending;
3719
+ 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);
3732
+ }
3733
+ };
3577
3734
 
3578
3735
  const wrappingWhiteSpace = ["pre-wrap", "normal", "pre-line"];
3579
3736
  class HeightOracle {
@@ -4702,7 +4859,7 @@ function buildTheme(main, spec, scopes) {
4702
4859
  });
4703
4860
  }
4704
4861
  const baseTheme = buildTheme("." + baseThemeID, {
4705
- "&": {
4862
+ "&.cm-editor": {
4706
4863
  position: "relative !important",
4707
4864
  boxSizing: "border-box",
4708
4865
  "&.cm-focused": {
@@ -4911,7 +5068,7 @@ class DOMObserver {
4911
5068
  this.intersection = new IntersectionObserver(entries => {
4912
5069
  if (this.parentCheck < 0)
4913
5070
  this.parentCheck = setTimeout(this.listenForScroll.bind(this), 1000);
4914
- if (entries[entries.length - 1].intersectionRatio > 0 != this.intersecting) {
5071
+ if (entries.length > 0 && entries[entries.length - 1].intersectionRatio > 0 != this.intersecting) {
4915
5072
  this.intersecting = !this.intersecting;
4916
5073
  if (this.intersecting != this.view.inView)
4917
5074
  this.onScrollChanged(document.createEvent("Event"));
@@ -4919,7 +5076,7 @@ class DOMObserver {
4919
5076
  }, {});
4920
5077
  this.intersection.observe(this.dom);
4921
5078
  this.gapIntersection = new IntersectionObserver(entries => {
4922
- if (entries[entries.length - 1].intersectionRatio > 0)
5079
+ if (entries.length > 0 && entries[entries.length - 1].intersectionRatio > 0)
4923
5080
  this.onScrollChanged(document.createEvent("Event"));
4924
5081
  }, {});
4925
5082
  }
@@ -5053,20 +5210,12 @@ class DOMObserver {
5053
5210
  this.flush();
5054
5211
  }
5055
5212
  }
5056
- // Apply pending changes, if any
5057
- flush() {
5058
- if (this.delayedFlush >= 0)
5059
- return;
5060
- this.lastFlush = Date.now();
5213
+ processRecords() {
5061
5214
  let records = this.queue;
5062
5215
  for (let mut of this.observer.takeRecords())
5063
5216
  records.push(mut);
5064
5217
  if (records.length)
5065
5218
  this.queue = [];
5066
- let selection = this.selectionRange;
5067
- let newSel = !this.ignoreSelection.eq(selection) && hasSelection(this.dom, selection);
5068
- if (records.length == 0 && !newSel)
5069
- return;
5070
5219
  let from = -1, to = -1, typeOver = false;
5071
5220
  for (let record of records) {
5072
5221
  let range = this.readMutation(record);
@@ -5082,17 +5231,26 @@ class DOMObserver {
5082
5231
  to = Math.max(range.to, to);
5083
5232
  }
5084
5233
  }
5234
+ return { from, to, typeOver };
5235
+ }
5236
+ // Apply pending changes, if any
5237
+ flush() {
5238
+ // Completely hold off flushing when pending keys are set—the code
5239
+ // managing those will make sure processRecords is called and the
5240
+ // view is resynchronized after
5241
+ if (this.delayedFlush >= 0 || this.view.inputState.pendingKey)
5242
+ return;
5243
+ this.lastFlush = Date.now();
5244
+ let { from, to, typeOver } = this.processRecords();
5245
+ let selection = this.selectionRange;
5246
+ let newSel = !this.ignoreSelection.eq(selection) && hasSelection(this.dom, selection);
5247
+ if (from < 0 && !newSel)
5248
+ return;
5085
5249
  let startState = this.view.state;
5086
- if (from > -1 || newSel)
5087
- this.onChange(from, to, typeOver);
5088
- if (this.view.state == startState) { // The view wasn't updated
5089
- if (this.view.docView.dirty) {
5090
- this.ignore(() => this.view.docView.sync());
5091
- this.view.docView.dirty = 0 /* Not */;
5092
- }
5093
- if (newSel)
5094
- this.view.docView.updateSelection();
5095
- }
5250
+ this.onChange(from, to, typeOver);
5251
+ // The view wasn't updated
5252
+ if (this.view.state == startState)
5253
+ this.view.docView.reset(newSel);
5096
5254
  this.clearSelection();
5097
5255
  }
5098
5256
  readMutation(rec) {
@@ -5229,9 +5387,9 @@ function applyDOMChange(view, start, end, typeOver) {
5229
5387
  (change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 &&
5230
5388
  dispatchKey(view.contentDOM, "Backspace", 8)) ||
5231
5389
  (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
5232
- dispatchKey(view.contentDOM, "Delete", 46))) ||
5233
- browser.ios && view.inputState.flushIOSKey(view))
5390
+ dispatchKey(view.contentDOM, "Delete", 46)))) {
5234
5391
  return;
5392
+ }
5235
5393
  let text = change.insert.toString();
5236
5394
  if (view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text)))
5237
5395
  return;
@@ -5452,7 +5610,7 @@ class EditorView {
5452
5610
  this.dom.appendChild(this.scrollDOM);
5453
5611
  this._dispatch = config.dispatch || ((tr) => this.update([tr]));
5454
5612
  this.dispatch = this.dispatch.bind(this);
5455
- this.root = (config.root || document);
5613
+ this.root = (config.root || getRoot(config.parent) || document);
5456
5614
  this.viewState = new ViewState(config.state || state.EditorState.create());
5457
5615
  this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec).update(this));
5458
5616
  this.observer = new DOMObserver(this, (from, to, typeOver) => {
@@ -5720,6 +5878,7 @@ class EditorView {
5720
5878
  spellcheck: "false",
5721
5879
  autocorrect: "off",
5722
5880
  autocapitalize: "off",
5881
+ translate: "no",
5723
5882
  contenteditable: !this.state.facet(editable) ? "false" : contentEditablePlainTextSupported() ? "plaintext-only" : "true",
5724
5883
  class: "cm-content",
5725
5884
  style: `${browser.tabSize}: ${this.state.tabSize}`,
@@ -6220,11 +6379,7 @@ class CachedOrder {
6220
6379
  }
6221
6380
  }
6222
6381
 
6223
- const currentPlatform = typeof navigator == "undefined" ? "key"
6224
- : /Mac/.test(navigator.platform) ? "mac"
6225
- : /Win/.test(navigator.platform) ? "win"
6226
- : /Linux|X11/.test(navigator.platform) ? "linux"
6227
- : "key";
6382
+ const currentPlatform = browser.mac ? "mac" : browser.windows ? "win" : browser.linux ? "linux" : "key";
6228
6383
  function normalizeKeyName(name, platform) {
6229
6384
  const parts = name.split(/-(?!$)/);
6230
6385
  let result = parts[parts.length - 1];
@@ -6730,7 +6885,7 @@ class MatchDecorator {
6730
6885
  }
6731
6886
 
6732
6887
  const UnicodeRegexpSupport = /x/.unicode != null ? "gu" : "g";
6733
- const Specials = new RegExp("[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]", UnicodeRegexpSupport);
6888
+ const Specials = new RegExp("[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\ufeff\ufff9-\ufffc]", UnicodeRegexpSupport);
6734
6889
  const Names = {
6735
6890
  0: "null",
6736
6891
  7: "bell",
@@ -6745,6 +6900,8 @@ const Names = {
6745
6900
  8206: "left-to-right mark",
6746
6901
  8207: "right-to-left mark",
6747
6902
  8232: "line separator",
6903
+ 8237: "left-to-right override",
6904
+ 8238: "right-to-left override",
6748
6905
  8233: "paragraph separator",
6749
6906
  65279: "zero width no-break space",
6750
6907
  65532: "object replacement"
@@ -6911,7 +7068,7 @@ DOM class.
6911
7068
  function highlightActiveLine() {
6912
7069
  return activeLineHighlighter;
6913
7070
  }
6914
- const lineDeco = Decoration.line({ attributes: { class: "cm-activeLine" } });
7071
+ const lineDeco = Decoration.line({ class: "cm-activeLine" });
6915
7072
  const activeLineHighlighter = ViewPlugin.fromClass(class {
6916
7073
  constructor(view) {
6917
7074
  this.decorations = this.getDeco(view);
package/dist/index.d.ts CHANGED
@@ -119,6 +119,10 @@ interface LineDecorationSpec {
119
119
  [key: string]: string;
120
120
  };
121
121
  /**
122
+ Shorthand for `{attributes: {class: value}}`.
123
+ */
124
+ class?: string;
125
+ /**
122
126
  Other properties are allowed.
123
127
  */
124
128
  [other: string]: any;
@@ -599,7 +603,9 @@ interface EditorConfig {
599
603
  /**
600
604
  If the view is going to be mounted in a shadow root or document
601
605
  other than the one held by the global variable `document` (the
602
- default), you should pass it here.
606
+ default), you should pass it here. If you provide `parent`, but
607
+ not this option, the editor will automatically look up a root
608
+ from the parent.
603
609
  */
604
610
  root?: Document | ShadowRoot;
605
611
  /**
package/dist/index.js CHANGED
@@ -248,6 +248,14 @@ function contentEditablePlainTextSupported() {
248
248
  }
249
249
  return _plainTextSupported;
250
250
  }
251
+ function getRoot(node) {
252
+ while (node) {
253
+ node = node.assignedSlot || node.parentNode;
254
+ if (node && (node.nodeType == 9 || node.nodeType == 11 && node.host))
255
+ return node;
256
+ }
257
+ return null;
258
+ }
251
259
 
252
260
  class DOMPos {
253
261
  constructor(node, offset, precise = true) {
@@ -445,6 +453,7 @@ class ContentView {
445
453
  (this.breakAfter ? "#" : "");
446
454
  }
447
455
  static get(node) { return node.cmView; }
456
+ get isEditable() { return true; }
448
457
  }
449
458
  ContentView.prototype.breakAfter = 0;
450
459
  // Remove a DOM node and return its next sibling.
@@ -492,15 +501,18 @@ const gecko = !ie && /*@__PURE__*//gecko\/(\d+)/i.test(nav.userAgent);
492
501
  const chrome = !ie && /*@__PURE__*//Chrome\/(\d+)/.exec(nav.userAgent);
493
502
  const webkit = "webkitFontSmoothing" in doc.documentElement.style;
494
503
  const safari = !ie && /*@__PURE__*//Apple Computer/.test(nav.vendor);
504
+ const ios = safari && (/*@__PURE__*//Mobile\/\w+/.test(nav.userAgent) || nav.maxTouchPoints > 2);
495
505
  var browser = {
496
- mac: /*@__PURE__*//Mac/.test(nav.platform),
506
+ mac: ios || /*@__PURE__*//Mac/.test(nav.platform),
507
+ windows: /*@__PURE__*//Win/.test(nav.platform),
508
+ linux: /*@__PURE__*//Linux|X11/.test(nav.platform),
497
509
  ie,
498
510
  ie_version: ie_upto10 ? doc.documentMode || 6 : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0,
499
511
  gecko,
500
512
  gecko_version: gecko ? +(/*@__PURE__*//Firefox\/(\d+)/.exec(nav.userAgent) || [0, 0])[1] : 0,
501
513
  chrome: !!chrome,
502
514
  chrome_version: chrome ? +chrome[1] : 0,
503
- ios: safari && (/*@__PURE__*//Mobile\/\w+/.test(nav.userAgent) || nav.maxTouchPoints > 2),
515
+ ios,
504
516
  android: /*@__PURE__*//Android\b/.test(nav.userAgent),
505
517
  webkit,
506
518
  safari,
@@ -722,6 +734,7 @@ class WidgetView extends InlineView {
722
734
  }
723
735
  return (pos == 0 && side > 0 || pos == this.length && side <= 0) ? rect : flattenRect(rect, pos == 0);
724
736
  }
737
+ get isEditable() { return false; }
725
738
  }
726
739
  class CompositionView extends WidgetView {
727
740
  domAtPos(pos) { return new DOMPos(this.widget.text, pos); }
@@ -733,6 +746,38 @@ class CompositionView extends WidgetView {
733
746
  ignoreMutation() { return false; }
734
747
  get overrideDOMText() { return null; }
735
748
  coordsAt(pos, side) { return textCoords(this.widget.text, pos, side); }
749
+ get isEditable() { return true; }
750
+ }
751
+ // These are drawn around uneditable widgets to avoid a number of
752
+ // browser bugs that show up when the cursor is directly next to
753
+ // uneditable inline content.
754
+ class WidgetBufferView extends InlineView {
755
+ constructor(side) {
756
+ super();
757
+ this.side = side;
758
+ }
759
+ get length() { return 0; }
760
+ merge() { return false; }
761
+ become(other) {
762
+ return other instanceof WidgetBufferView && other.side == this.side;
763
+ }
764
+ slice() { return new WidgetBufferView(this.side); }
765
+ sync() {
766
+ if (!this.dom)
767
+ this.setDOM(document.createTextNode("\u200b"));
768
+ else if (this.dirty && this.dom.nodeValue != "\u200b")
769
+ this.dom.nodeValue = "\u200b";
770
+ }
771
+ getSide() { return this.side; }
772
+ domAtPos(pos) { return DOMPos.before(this.dom); }
773
+ domBoundsAround() { return null; }
774
+ coordsAt(pos) {
775
+ let rects = clientRectsFor(this.dom);
776
+ return rects[rects.length - 1];
777
+ }
778
+ get overrideDOMText() {
779
+ return Text.of([this.dom.nodeValue.replace(/\u200b/g, "")]);
780
+ }
736
781
  }
737
782
  function mergeInlineChildren(parent, from, to, elts, openStart, openEnd) {
738
783
  let cur = parent.childCursor();
@@ -1227,14 +1272,17 @@ class LineView extends ContentView {
1227
1272
  }
1228
1273
  // Only called when building a line view in ContentBuilder
1229
1274
  addLineDeco(deco) {
1230
- let attrs = deco.spec.attributes;
1275
+ let attrs = deco.spec.attributes, cls = deco.spec.class;
1231
1276
  if (attrs)
1232
1277
  this.attrs = combineAttrs(attrs, this.attrs || {});
1278
+ if (cls)
1279
+ this.attrs = combineAttrs(attrs, { class: cls });
1233
1280
  }
1234
1281
  domAtPos(pos) {
1235
1282
  return inlineDOMAtPos(this.dom, this.children, pos);
1236
1283
  }
1237
1284
  sync(track) {
1285
+ var _a;
1238
1286
  if (!this.dom || (this.dirty & 4 /* Attrs */)) {
1239
1287
  this.setDOM(document.createElement("div"));
1240
1288
  this.dom.className = "cm-line";
@@ -1250,7 +1298,7 @@ class LineView extends ContentView {
1250
1298
  while (last && ContentView.get(last) instanceof MarkView)
1251
1299
  last = last.lastChild;
1252
1300
  if (!last ||
1253
- last.nodeName != "BR" && ContentView.get(last) instanceof WidgetView &&
1301
+ last.nodeName != "BR" && ((_a = ContentView.get(last)) === null || _a === void 0 ? void 0 : _a.isEditable) == false &&
1254
1302
  (!browser.ios || !this.children.some(ch => ch instanceof TextView))) {
1255
1303
  let hack = document.createElement("BR");
1256
1304
  hack.cmIgnore = true;
@@ -1349,6 +1397,9 @@ class ContentBuilder {
1349
1397
  this.content = [];
1350
1398
  this.curLine = null;
1351
1399
  this.breakAtStart = 0;
1400
+ this.pendingBuffer = 0 /* No */;
1401
+ // Set to false directly after a widget that covers the position after it
1402
+ this.atCursorPos = true;
1352
1403
  this.openStart = -1;
1353
1404
  this.openEnd = -1;
1354
1405
  this.text = "";
@@ -1363,23 +1414,31 @@ class ContentBuilder {
1363
1414
  return !last.breakAfter && !(last instanceof BlockWidgetView && last.type == BlockType.WidgetBefore);
1364
1415
  }
1365
1416
  getLine() {
1366
- if (!this.curLine)
1417
+ if (!this.curLine) {
1367
1418
  this.content.push(this.curLine = new LineView);
1419
+ this.atCursorPos = true;
1420
+ }
1368
1421
  return this.curLine;
1369
1422
  }
1370
- addWidget(view) {
1423
+ flushBuffer(active) {
1424
+ if (this.pendingBuffer) {
1425
+ this.curLine.append(wrapMarks(new WidgetBufferView(-1), active), active.length);
1426
+ this.pendingBuffer = 0 /* No */;
1427
+ }
1428
+ }
1429
+ addBlockWidget(view) {
1430
+ this.flushBuffer([]);
1371
1431
  this.curLine = null;
1372
1432
  this.content.push(view);
1373
1433
  }
1374
- finish() {
1434
+ finish(openEnd) {
1435
+ if (!openEnd)
1436
+ this.flushBuffer([]);
1437
+ else
1438
+ this.pendingBuffer = 0 /* No */;
1375
1439
  if (!this.posCovered())
1376
1440
  this.getLine();
1377
1441
  }
1378
- wrapMarks(view, active) {
1379
- for (let mark of active)
1380
- view = new MarkView(mark, [view], view.length);
1381
- return view;
1382
- }
1383
1442
  buildText(length, active, openStart) {
1384
1443
  while (length > 0) {
1385
1444
  if (this.textOff == this.text.length) {
@@ -1394,6 +1453,7 @@ class ContentBuilder {
1394
1453
  this.content[this.content.length - 1].breakAfter = 1;
1395
1454
  else
1396
1455
  this.breakAtStart = 1;
1456
+ this.flushBuffer([]);
1397
1457
  this.curLine = null;
1398
1458
  length--;
1399
1459
  continue;
@@ -1404,7 +1464,9 @@ class ContentBuilder {
1404
1464
  }
1405
1465
  }
1406
1466
  let take = Math.min(this.text.length - this.textOff, length, 512 /* Chunk */);
1407
- this.getLine().append(this.wrapMarks(new TextView(this.text.slice(this.textOff, this.textOff + take)), active), openStart);
1467
+ this.flushBuffer(active);
1468
+ this.getLine().append(wrapMarks(new TextView(this.text.slice(this.textOff, this.textOff + take)), active), openStart);
1469
+ this.atCursorPos = true;
1408
1470
  this.textOff += take;
1409
1471
  length -= take;
1410
1472
  openStart = 0;
@@ -1423,11 +1485,23 @@ class ContentBuilder {
1423
1485
  let { type } = deco;
1424
1486
  if (type == BlockType.WidgetAfter && !this.posCovered())
1425
1487
  this.getLine();
1426
- this.addWidget(new BlockWidgetView(deco.widget || new NullWidget("div"), len, type));
1488
+ this.addBlockWidget(new BlockWidgetView(deco.widget || new NullWidget("div"), len, type));
1427
1489
  }
1428
1490
  else {
1429
- let widget = this.wrapMarks(WidgetView.create(deco.widget || new NullWidget("span"), len, deco.startSide), active);
1430
- this.getLine().append(widget, openStart);
1491
+ let view = WidgetView.create(deco.widget || new NullWidget("span"), len, deco.startSide);
1492
+ let cursorBefore = this.atCursorPos && !view.isEditable && openStart <= active.length && (from < to || deco.startSide > 0);
1493
+ let cursorAfter = !view.isEditable && (from < to || deco.startSide <= 0);
1494
+ let line = this.getLine();
1495
+ if (this.pendingBuffer == 2 /* IfCursor */ && !cursorBefore)
1496
+ this.pendingBuffer = 0 /* No */;
1497
+ this.flushBuffer(active);
1498
+ if (cursorBefore) {
1499
+ line.append(wrapMarks(new WidgetBufferView(1), active), openStart);
1500
+ openStart = active.length + Math.max(0, openStart - active.length);
1501
+ }
1502
+ line.append(wrapMarks(view, active), openStart);
1503
+ this.atCursorPos = cursorAfter;
1504
+ this.pendingBuffer = !cursorAfter ? 0 /* No */ : from < to ? 1 /* Yes */ : 2 /* IfCursor */;
1431
1505
  }
1432
1506
  }
1433
1507
  else if (this.doc.lineAt(this.pos).from == this.pos) { // Line decoration
@@ -1453,10 +1527,15 @@ class ContentBuilder {
1453
1527
  builder.openEnd = RangeSet.spans(decorations, from, to, builder);
1454
1528
  if (builder.openStart < 0)
1455
1529
  builder.openStart = builder.openEnd;
1456
- builder.finish();
1530
+ builder.finish(builder.openEnd);
1457
1531
  return builder;
1458
1532
  }
1459
1533
  }
1534
+ function wrapMarks(view, active) {
1535
+ for (let mark of active)
1536
+ view = new MarkView(mark, [view], view.length);
1537
+ return view;
1538
+ }
1460
1539
  class NullWidget extends WidgetType {
1461
1540
  constructor(tag) {
1462
1541
  super();
@@ -1903,6 +1982,14 @@ class DocView extends ContentView {
1903
1982
  return true;
1904
1983
  }
1905
1984
  }
1985
+ reset(sel) {
1986
+ if (this.dirty) {
1987
+ this.view.observer.ignore(() => this.view.docView.sync());
1988
+ this.dirty = 0 /* Not */;
1989
+ }
1990
+ if (sel)
1991
+ this.updateSelection();
1992
+ }
1906
1993
  // Used both by update and checkLayout do perform the actual DOM
1907
1994
  // update
1908
1995
  updateInner(changes, deco, oldLength, forceSelection = false, pointerSel = false) {
@@ -2032,6 +2119,14 @@ class DocView extends ContentView {
2032
2119
  !isEquivalentPosition(anchor.node, anchor.offset, domSel.anchorNode, domSel.anchorOffset) ||
2033
2120
  !isEquivalentPosition(head.node, head.offset, domSel.focusNode, domSel.focusOffset)) {
2034
2121
  this.view.observer.ignore(() => {
2122
+ // Chrome Android will hide the virtual keyboard when tapping
2123
+ // inside an uneditable node, and not bring it back when we
2124
+ // move the cursor to its proper position. This tries to
2125
+ // restore the keyboard by cycling focus.
2126
+ if (browser.android && browser.chrome && this.dom.contains(domSel.focusNode) && inUneditable(domSel.focusNode, this.dom)) {
2127
+ this.dom.blur();
2128
+ this.dom.focus({ preventScroll: true });
2129
+ }
2035
2130
  let rawSel = getSelection(this.root);
2036
2131
  if (main.empty) {
2037
2132
  // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=1612076
@@ -2339,6 +2434,14 @@ function findChangedDeco(a, b, diff) {
2339
2434
  RangeSet.compare(a, b, diff, comp);
2340
2435
  return comp.changes;
2341
2436
  }
2437
+ function inUneditable(node, inside) {
2438
+ for (let cur = node; cur && cur != inside; cur = cur.assignedSlot || cur.parentNode) {
2439
+ if (cur.nodeType == 1 && cur.contentEditable == 'false') {
2440
+ return true;
2441
+ }
2442
+ }
2443
+ return false;
2444
+ }
2342
2445
 
2343
2446
  /**
2344
2447
  Used to indicate [text direction](https://codemirror.net/6/docs/ref/#view.EditorView.textDirection).
@@ -2784,6 +2887,7 @@ function domPosInText(node, x, y) {
2784
2887
  return { node, offset: closestOffset > -1 ? closestOffset : generalSide > 0 ? node.nodeValue.length : 0 };
2785
2888
  }
2786
2889
  function posAtCoords(view, { x, y }, precise, bias = -1) {
2890
+ var _a;
2787
2891
  let content = view.contentDOM.getBoundingClientRect(), block;
2788
2892
  let halfLine = view.defaultLineHeight / 2;
2789
2893
  for (let bounced = false;;) {
@@ -2814,7 +2918,7 @@ function posAtCoords(view, { x, y }, precise, bias = -1) {
2814
2918
  // There's visible editor content under the point, so we can try
2815
2919
  // using caret(Position|Range)FromPoint as a shortcut
2816
2920
  let node, offset = -1;
2817
- if (element && view.contentDOM.contains(element) && !(view.docView.nearest(element) instanceof WidgetView)) {
2921
+ if (element && view.contentDOM.contains(element) && ((_a = view.docView.nearest(element)) === null || _a === void 0 ? void 0 : _a.isEditable) != false) {
2818
2922
  if (doc.caretPositionFromPoint) {
2819
2923
  let pos = doc.caretPositionFromPoint(x, y);
2820
2924
  if (pos)
@@ -2955,7 +3059,23 @@ class InputState {
2955
3059
  constructor(view) {
2956
3060
  this.lastKeyCode = 0;
2957
3061
  this.lastKeyTime = 0;
2958
- this.pendingIOSKey = null;
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
+ // On Chrome Android, backspace near widgets is just completely
3067
+ // broken, and there are no key events, so we need to handle the
3068
+ // beforeinput event. Deleting stuff will often create a flurry of
3069
+ // events, and interrupting it before it is done just makes
3070
+ // subsequent events even more broken, so again, we hold off doing
3071
+ // anything until the browser is finished with whatever it is trying
3072
+ // 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;
2959
3079
  this.lastSelectionOrigin = null;
2960
3080
  this.lastSelectionTime = 0;
2961
3081
  this.lastEscPress = 0;
@@ -3064,20 +3184,27 @@ class InputState {
3064
3184
  // state. So we let it go through, and then, in
3065
3185
  // applyDOMChange, notify key handlers of it and reset to
3066
3186
  // the state they produce.
3067
- if (browser.ios && (event.keyCode == 13 || event.keyCode == 8) &&
3187
+ let pending;
3188
+ if (browser.ios && (pending = PendingKeys.find(key => key.keyCode == event.keyCode)) &&
3068
3189
  !(event.ctrlKey || event.altKey || event.metaKey) && !event.synthetic) {
3069
- this.pendingIOSKey = event.keyCode == 13 ? "enter" : "backspace";
3070
- setTimeout(() => this.flushIOSKey(view), 250);
3190
+ this.setPendingKey(view, pending);
3071
3191
  return true;
3072
3192
  }
3073
3193
  return false;
3074
3194
  }
3075
- flushIOSKey(view) {
3076
- if (!this.pendingIOSKey)
3077
- return false;
3078
- let dom = view.contentDOM, key = this.pendingIOSKey;
3079
- this.pendingIOSKey = null;
3080
- return key == "enter" ? dispatchKey(dom, "Enter", 13) : dispatchKey(dom, "Backspace", 8);
3195
+ setPendingKey(view, pending) {
3196
+ this.pendingKey = pending;
3197
+ requestAnimationFrame(() => {
3198
+ if (!this.pendingKey)
3199
+ return false;
3200
+ let key = this.pendingKey;
3201
+ this.pendingKey = undefined;
3202
+ view.observer.processRecords();
3203
+ let startState = view.state;
3204
+ dispatchKey(view.contentDOM, key.key, key.keyCode);
3205
+ if (view.state == startState)
3206
+ view.docView.reset(true);
3207
+ });
3081
3208
  }
3082
3209
  ignoreDuringComposition(event) {
3083
3210
  if (!/^key/.test(event.type))
@@ -3124,6 +3251,11 @@ class InputState {
3124
3251
  this.mouseSelection.destroy();
3125
3252
  }
3126
3253
  }
3254
+ const PendingKeys = [
3255
+ { key: "Backspace", keyCode: 8, inputType: "deleteContentBackward" },
3256
+ { key: "Enter", keyCode: 13, inputType: "insertParagraph" },
3257
+ { key: "Delete", keyCode: 46, inputType: "deleteContentForward" }
3258
+ ];
3127
3259
  // Key codes for modifier keys
3128
3260
  const modifierCodes = [16, 17, 18, 20, 91, 92, 224, 225];
3129
3261
  class MouseSelection {
@@ -3569,6 +3701,31 @@ handlers.compositionend = view => {
3569
3701
  handlers.contextmenu = view => {
3570
3702
  view.inputState.lastContextMenu = Date.now();
3571
3703
  };
3704
+ handlers.beforeinput = (view, event) => {
3705
+ var _a;
3706
+ // Because Chrome Android doesn't fire useful key events, use
3707
+ // beforeinput to detect backspace (and possibly enter and delete,
3708
+ // but those usually don't even seem to fire beforeinput events at
3709
+ // the moment) and fake a key event for it.
3710
+ //
3711
+ // (preventDefault on beforeinput, though supported in the spec,
3712
+ // seems to do nothing at all on Chrome).
3713
+ let pending;
3714
+ 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);
3727
+ }
3728
+ };
3572
3729
 
3573
3730
  const wrappingWhiteSpace = ["pre-wrap", "normal", "pre-line"];
3574
3731
  class HeightOracle {
@@ -4696,7 +4853,7 @@ function buildTheme(main, spec, scopes) {
4696
4853
  });
4697
4854
  }
4698
4855
  const baseTheme = /*@__PURE__*/buildTheme("." + baseThemeID, {
4699
- "&": {
4856
+ "&.cm-editor": {
4700
4857
  position: "relative !important",
4701
4858
  boxSizing: "border-box",
4702
4859
  "&.cm-focused": {
@@ -4905,7 +5062,7 @@ class DOMObserver {
4905
5062
  this.intersection = new IntersectionObserver(entries => {
4906
5063
  if (this.parentCheck < 0)
4907
5064
  this.parentCheck = setTimeout(this.listenForScroll.bind(this), 1000);
4908
- if (entries[entries.length - 1].intersectionRatio > 0 != this.intersecting) {
5065
+ if (entries.length > 0 && entries[entries.length - 1].intersectionRatio > 0 != this.intersecting) {
4909
5066
  this.intersecting = !this.intersecting;
4910
5067
  if (this.intersecting != this.view.inView)
4911
5068
  this.onScrollChanged(document.createEvent("Event"));
@@ -4913,7 +5070,7 @@ class DOMObserver {
4913
5070
  }, {});
4914
5071
  this.intersection.observe(this.dom);
4915
5072
  this.gapIntersection = new IntersectionObserver(entries => {
4916
- if (entries[entries.length - 1].intersectionRatio > 0)
5073
+ if (entries.length > 0 && entries[entries.length - 1].intersectionRatio > 0)
4917
5074
  this.onScrollChanged(document.createEvent("Event"));
4918
5075
  }, {});
4919
5076
  }
@@ -5047,20 +5204,12 @@ class DOMObserver {
5047
5204
  this.flush();
5048
5205
  }
5049
5206
  }
5050
- // Apply pending changes, if any
5051
- flush() {
5052
- if (this.delayedFlush >= 0)
5053
- return;
5054
- this.lastFlush = Date.now();
5207
+ processRecords() {
5055
5208
  let records = this.queue;
5056
5209
  for (let mut of this.observer.takeRecords())
5057
5210
  records.push(mut);
5058
5211
  if (records.length)
5059
5212
  this.queue = [];
5060
- let selection = this.selectionRange;
5061
- let newSel = !this.ignoreSelection.eq(selection) && hasSelection(this.dom, selection);
5062
- if (records.length == 0 && !newSel)
5063
- return;
5064
5213
  let from = -1, to = -1, typeOver = false;
5065
5214
  for (let record of records) {
5066
5215
  let range = this.readMutation(record);
@@ -5076,17 +5225,26 @@ class DOMObserver {
5076
5225
  to = Math.max(range.to, to);
5077
5226
  }
5078
5227
  }
5228
+ return { from, to, typeOver };
5229
+ }
5230
+ // Apply pending changes, if any
5231
+ flush() {
5232
+ // Completely hold off flushing when pending keys are set—the code
5233
+ // managing those will make sure processRecords is called and the
5234
+ // view is resynchronized after
5235
+ if (this.delayedFlush >= 0 || this.view.inputState.pendingKey)
5236
+ return;
5237
+ this.lastFlush = Date.now();
5238
+ let { from, to, typeOver } = this.processRecords();
5239
+ let selection = this.selectionRange;
5240
+ let newSel = !this.ignoreSelection.eq(selection) && hasSelection(this.dom, selection);
5241
+ if (from < 0 && !newSel)
5242
+ return;
5079
5243
  let startState = this.view.state;
5080
- if (from > -1 || newSel)
5081
- this.onChange(from, to, typeOver);
5082
- if (this.view.state == startState) { // The view wasn't updated
5083
- if (this.view.docView.dirty) {
5084
- this.ignore(() => this.view.docView.sync());
5085
- this.view.docView.dirty = 0 /* Not */;
5086
- }
5087
- if (newSel)
5088
- this.view.docView.updateSelection();
5089
- }
5244
+ this.onChange(from, to, typeOver);
5245
+ // The view wasn't updated
5246
+ if (this.view.state == startState)
5247
+ this.view.docView.reset(newSel);
5090
5248
  this.clearSelection();
5091
5249
  }
5092
5250
  readMutation(rec) {
@@ -5223,9 +5381,9 @@ function applyDOMChange(view, start, end, typeOver) {
5223
5381
  (change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 &&
5224
5382
  dispatchKey(view.contentDOM, "Backspace", 8)) ||
5225
5383
  (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
5226
- dispatchKey(view.contentDOM, "Delete", 46))) ||
5227
- browser.ios && view.inputState.flushIOSKey(view))
5384
+ dispatchKey(view.contentDOM, "Delete", 46)))) {
5228
5385
  return;
5386
+ }
5229
5387
  let text = change.insert.toString();
5230
5388
  if (view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text)))
5231
5389
  return;
@@ -5446,7 +5604,7 @@ class EditorView {
5446
5604
  this.dom.appendChild(this.scrollDOM);
5447
5605
  this._dispatch = config.dispatch || ((tr) => this.update([tr]));
5448
5606
  this.dispatch = this.dispatch.bind(this);
5449
- this.root = (config.root || document);
5607
+ this.root = (config.root || getRoot(config.parent) || document);
5450
5608
  this.viewState = new ViewState(config.state || EditorState.create());
5451
5609
  this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec).update(this));
5452
5610
  this.observer = new DOMObserver(this, (from, to, typeOver) => {
@@ -5714,6 +5872,7 @@ class EditorView {
5714
5872
  spellcheck: "false",
5715
5873
  autocorrect: "off",
5716
5874
  autocapitalize: "off",
5875
+ translate: "no",
5717
5876
  contenteditable: !this.state.facet(editable) ? "false" : contentEditablePlainTextSupported() ? "plaintext-only" : "true",
5718
5877
  class: "cm-content",
5719
5878
  style: `${browser.tabSize}: ${this.state.tabSize}`,
@@ -6214,11 +6373,7 @@ class CachedOrder {
6214
6373
  }
6215
6374
  }
6216
6375
 
6217
- const currentPlatform = typeof navigator == "undefined" ? "key"
6218
- : /*@__PURE__*//Mac/.test(navigator.platform) ? "mac"
6219
- : /*@__PURE__*//Win/.test(navigator.platform) ? "win"
6220
- : /*@__PURE__*//Linux|X11/.test(navigator.platform) ? "linux"
6221
- : "key";
6376
+ const currentPlatform = browser.mac ? "mac" : browser.windows ? "win" : browser.linux ? "linux" : "key";
6222
6377
  function normalizeKeyName(name, platform) {
6223
6378
  const parts = name.split(/-(?!$)/);
6224
6379
  let result = parts[parts.length - 1];
@@ -6724,7 +6879,7 @@ class MatchDecorator {
6724
6879
  }
6725
6880
 
6726
6881
  const UnicodeRegexpSupport = /x/.unicode != null ? "gu" : "g";
6727
- const Specials = /*@__PURE__*/new RegExp("[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]", UnicodeRegexpSupport);
6882
+ const Specials = /*@__PURE__*/new RegExp("[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\ufeff\ufff9-\ufffc]", UnicodeRegexpSupport);
6728
6883
  const Names = {
6729
6884
  0: "null",
6730
6885
  7: "bell",
@@ -6739,6 +6894,8 @@ const Names = {
6739
6894
  8206: "left-to-right mark",
6740
6895
  8207: "right-to-left mark",
6741
6896
  8232: "line separator",
6897
+ 8237: "left-to-right override",
6898
+ 8238: "right-to-left override",
6742
6899
  8233: "paragraph separator",
6743
6900
  65279: "zero width no-break space",
6744
6901
  65532: "object replacement"
@@ -6905,7 +7062,7 @@ DOM class.
6905
7062
  function highlightActiveLine() {
6906
7063
  return activeLineHighlighter;
6907
7064
  }
6908
- const lineDeco = /*@__PURE__*/Decoration.line({ attributes: { class: "cm-activeLine" } });
7065
+ const lineDeco = /*@__PURE__*/Decoration.line({ class: "cm-activeLine" });
6909
7066
  const activeLineHighlighter = /*@__PURE__*/ViewPlugin.fromClass(class {
6910
7067
  constructor(view) {
6911
7068
  this.decorations = this.getDeco(view);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "0.19.9",
3
+ "version": "0.19.10",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",