@codemirror/view 6.0.2 → 6.1.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/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ ## 6.1.1 (2022-07-25)
2
+
3
+ ### Bug fixes
4
+
5
+ Make `highlightSpecialChars` replace directional isolate characters by default.
6
+
7
+ The editor will now try to suppress browsers' native behavior of resetting the selection in the editable content when the editable element is focused (programmatically, with tab, etc).
8
+
9
+ Fix a CSS issue that made it possible, when the gutters were wide enough, for them to overlap with the content.
10
+
11
+ ## 6.1.0 (2022-07-19)
12
+
13
+ ### New features
14
+
15
+ `MatchDecorator` now supports a `decorate` option that can be used to customize the way decorations are added for each match.
16
+
17
+ ## 6.0.3 (2022-07-08)
18
+
19
+ ### Bug fixes
20
+
21
+ Fix a problem where `posAtCoords` could incorrectly return the start of the next line when querying positions between lines.
22
+
23
+ Fix an issue where registering a high-precedence keymap made keymap handling take precedence over other keydown event handlers.
24
+
25
+ Ctrl/Cmd-clicking can now remove ranges from a multi-range selection.
26
+
1
27
  ## 6.0.2 (2022-06-23)
2
28
 
3
29
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -268,6 +268,31 @@ function clearAttributes(node) {
268
268
  while (node.attributes.length)
269
269
  node.removeAttributeNode(node.attributes[0]);
270
270
  }
271
+ function atElementStart(doc, selection) {
272
+ let node = selection.focusNode, offset = selection.focusOffset;
273
+ if (!node || selection.anchorNode != node || selection.anchorOffset != offset)
274
+ return false;
275
+ for (;;) {
276
+ if (offset) {
277
+ if (node.nodeType != 1)
278
+ return false;
279
+ let prev = node.childNodes[offset - 1];
280
+ if (prev.contentEditable == "false")
281
+ offset--;
282
+ else {
283
+ node = prev;
284
+ offset = maxOffset(node);
285
+ }
286
+ }
287
+ else if (node == doc) {
288
+ return true;
289
+ }
290
+ else {
291
+ offset = domIndex(node);
292
+ node = node.parentNode;
293
+ }
294
+ }
295
+ }
271
296
 
272
297
  class DOMPos {
273
298
  constructor(node, offset, precise = true) {
@@ -3137,7 +3162,8 @@ function posAtCoords(view, { x, y }, precise, bias = -1) {
3137
3162
  let range = doc.caretRangeFromPoint(x, y);
3138
3163
  if (range) {
3139
3164
  ({ startContainer: node, startOffset: offset } = range);
3140
- if (browser.safari && isSuspiciousCaretResult(node, offset, x))
3165
+ if (browser.safari && isSuspiciousSafariCaretResult(node, offset, x) ||
3166
+ browser.chrome && isSuspiciousChromeCaretResult(node, offset, x))
3141
3167
  node = undefined;
3142
3168
  }
3143
3169
  }
@@ -3164,7 +3190,7 @@ function posAtCoordsImprecise(view, contentRect, block, x, y) {
3164
3190
  // the space between lines as belonging to the last character of the
3165
3191
  // line before. This is used to detect such a result so that it can be
3166
3192
  // ignored (issue #401).
3167
- function isSuspiciousCaretResult(node, offset, x) {
3193
+ function isSuspiciousSafariCaretResult(node, offset, x) {
3168
3194
  let len;
3169
3195
  if (node.nodeType != 3 || offset != (len = node.nodeValue.length))
3170
3196
  return false;
@@ -3173,6 +3199,22 @@ function isSuspiciousCaretResult(node, offset, x) {
3173
3199
  return false;
3174
3200
  return textRange(node, len - 1, len).getBoundingClientRect().left > x;
3175
3201
  }
3202
+ // Chrome will move positions between lines to the start of the next line
3203
+ function isSuspiciousChromeCaretResult(node, offset, x) {
3204
+ if (offset != 0)
3205
+ return false;
3206
+ for (let cur = node;;) {
3207
+ let parent = cur.parentNode;
3208
+ if (!parent || parent.nodeType != 1 || parent.firstChild != cur)
3209
+ return false;
3210
+ if (parent.classList.contains("cm-line"))
3211
+ break;
3212
+ cur = parent;
3213
+ }
3214
+ let rect = node.nodeType == 1 ? node.getBoundingClientRect()
3215
+ : textRange(node, 0, Math.max(node.nodeValue.length, 1)).getBoundingClientRect();
3216
+ return x - rect.left > 5;
3217
+ }
3176
3218
  function moveToLineBoundary(view, start, forward, includeWrap) {
3177
3219
  let line = view.state.doc.lineAt(start.head);
3178
3220
  let coords = !includeWrap || !view.lineWrapping ? null
@@ -3272,6 +3314,10 @@ class InputState {
3272
3314
  constructor(view) {
3273
3315
  this.lastKeyCode = 0;
3274
3316
  this.lastKeyTime = 0;
3317
+ this.lastTouchTime = 0;
3318
+ this.lastFocusTime = 0;
3319
+ this.lastScrollTop = 0;
3320
+ this.lastScrollLeft = 0;
3275
3321
  this.chromeScrollHack = -1;
3276
3322
  // On iOS, some keys need to have their default behavior happen
3277
3323
  // (after which we retroactively handle them and reset the DOM) to
@@ -3373,6 +3419,8 @@ class InputState {
3373
3419
  return false;
3374
3420
  }
3375
3421
  runScrollHandlers(view, event) {
3422
+ this.lastScrollTop = view.scrollDOM.scrollTop;
3423
+ this.lastScrollLeft = view.scrollDOM.scrollLeft;
3376
3424
  for (let set of this.customHandlers) {
3377
3425
  let handler = set.handlers.scroll;
3378
3426
  if (handler) {
@@ -3617,9 +3665,8 @@ handlers.keydown = (view, event) => {
3617
3665
  else if (modifierCodes.indexOf(event.keyCode) < 0)
3618
3666
  view.inputState.lastEscPress = 0;
3619
3667
  };
3620
- let lastTouch = 0;
3621
3668
  handlers.touchstart = (view, e) => {
3622
- lastTouch = Date.now();
3669
+ view.inputState.lastTouchTime = Date.now();
3623
3670
  view.inputState.setSelectionOrigin("select.pointer");
3624
3671
  };
3625
3672
  handlers.touchmove = view => {
@@ -3627,7 +3674,7 @@ handlers.touchmove = view => {
3627
3674
  };
3628
3675
  handlers.mousedown = (view, event) => {
3629
3676
  view.observer.flush();
3630
- if (lastTouch > Date.now() - 2000 && getClickType(event) == 1)
3677
+ if (view.inputState.lastTouchTime > Date.now() - 2000 && getClickType(event) == 1)
3631
3678
  return; // Ignore touch interaction
3632
3679
  let style = null;
3633
3680
  for (let makeStyle of view.state.facet(mouseSelectionStyle)) {
@@ -3731,6 +3778,8 @@ function basicMouseSelection(view, event) {
3731
3778
  }
3732
3779
  if (extend)
3733
3780
  return startSel.replaceRange(startSel.main.extend(range.from, range.to));
3781
+ else if (multiple && startSel.ranges.length > 1 && startSel.ranges.some(r => r.eq(range)))
3782
+ return removeRange(startSel, range);
3734
3783
  else if (multiple)
3735
3784
  return startSel.addRange(range);
3736
3785
  else
@@ -3738,6 +3787,12 @@ function basicMouseSelection(view, event) {
3738
3787
  }
3739
3788
  };
3740
3789
  }
3790
+ function removeRange(sel, range) {
3791
+ for (let i = 0;; i++) {
3792
+ if (sel.ranges[i].eq(range))
3793
+ return state.EditorSelection.create(sel.ranges.slice(0, i).concat(sel.ranges.slice(i + 1)), sel.mainIndex == i ? 0 : sel.mainIndex - (sel.mainIndex > i ? 1 : 0));
3794
+ }
3795
+ }
3741
3796
  handlers.dragstart = (view, event) => {
3742
3797
  let { selection: { main } } = view.state;
3743
3798
  let { mouseSelection } = view.inputState;
@@ -3873,7 +3928,15 @@ function updateForFocusChange(view) {
3873
3928
  view.update([]);
3874
3929
  }, 10);
3875
3930
  }
3876
- handlers.focus = updateForFocusChange;
3931
+ handlers.focus = view => {
3932
+ view.inputState.lastFocusTime = Date.now();
3933
+ // When focusing reset the scroll position, move it back to where it was
3934
+ if (!view.scrollDOM.scrollTop && (view.inputState.lastScrollTop || view.inputState.lastScrollLeft)) {
3935
+ view.scrollDOM.scrollTop = view.inputState.lastScrollTop;
3936
+ view.scrollDOM.scrollLeft = view.inputState.lastScrollLeft;
3937
+ }
3938
+ updateForFocusChange(view);
3939
+ };
3877
3940
  handlers.blur = view => {
3878
3941
  view.observer.clearSelectionRange();
3879
3942
  updateForFocusChange(view);
@@ -5222,8 +5285,8 @@ const baseTheme$1 = buildTheme("." + baseThemeID, {
5222
5285
  // Two animations defined so that we can switch between them to
5223
5286
  // restart the animation without forcing another style
5224
5287
  // recomputation.
5225
- "@keyframes cm-blink": { "0%": {}, "50%": { visibility: "hidden" }, "100%": {} },
5226
- "@keyframes cm-blink2": { "0%": {}, "50%": { visibility: "hidden" }, "100%": {} },
5288
+ "@keyframes cm-blink": { "0%": {}, "50%": { opacity: 0 }, "100%": {} },
5289
+ "@keyframes cm-blink2": { "0%": {}, "50%": { opacity: 0 }, "100%": {} },
5227
5290
  ".cm-cursor, .cm-dropCursor": {
5228
5291
  position: "absolute",
5229
5292
  borderLeft: "1.2px solid black",
@@ -5244,6 +5307,7 @@ const baseTheme$1 = buildTheme("." + baseThemeID, {
5244
5307
  "&light .cm-specialChar": { color: "red" },
5245
5308
  "&dark .cm-specialChar": { color: "#f78" },
5246
5309
  ".cm-gutters": {
5310
+ flexShrink: 0,
5247
5311
  display: "flex",
5248
5312
  height: "100%",
5249
5313
  boxSizing: "border-box",
@@ -5500,15 +5564,28 @@ class DOMObserver {
5500
5564
  this.flush(false);
5501
5565
  }
5502
5566
  readSelectionRange() {
5503
- let { root } = this.view;
5567
+ let { view } = this;
5504
5568
  // The Selection object is broken in shadow roots in Safari. See
5505
5569
  // https://github.com/codemirror/dev/issues/414
5506
- let range = browser.safari && root.nodeType == 11 && deepActiveElement() == this.view.contentDOM &&
5507
- safariSelectionRangeHack(this.view) || getSelection(root);
5570
+ let range = browser.safari && view.root.nodeType == 11 && deepActiveElement() == this.dom &&
5571
+ safariSelectionRangeHack(this.view) || getSelection(view.root);
5508
5572
  if (!range || this.selectionRange.eq(range))
5509
5573
  return false;
5574
+ let local = hasSelection(this.dom, range);
5575
+ // Detect the situation where the browser has, on focus, moved the
5576
+ // selection to the start of the content element. Reset it to the
5577
+ // position from the editor state.
5578
+ if (local && !this.selectionChanged && this.selectionRange.focusNode &&
5579
+ view.inputState.lastFocusTime > Date.now() - 200 &&
5580
+ view.inputState.lastTouchTime < Date.now() - 300 &&
5581
+ atElementStart(this.dom, range)) {
5582
+ view.docView.updateSelection();
5583
+ return false;
5584
+ }
5510
5585
  this.selectionRange.setRange(range);
5511
- return this.selectionChanged = true;
5586
+ if (local)
5587
+ this.selectionChanged = true;
5588
+ return true;
5512
5589
  }
5513
5590
  setSelectionRange(anchor, head) {
5514
5591
  this.selectionRange.set(anchor.node, anchor.offset, head.node, head.offset);
@@ -5595,7 +5672,7 @@ class DOMObserver {
5595
5672
  this.delayedAndroidKey = null;
5596
5673
  this.delayedFlush = -1;
5597
5674
  if (!this.flush())
5598
- dispatchKey(this.view.contentDOM, key.key, key.keyCode);
5675
+ dispatchKey(this.dom, key.key, key.keyCode);
5599
5676
  });
5600
5677
  // Since backspace beforeinput is sometimes signalled spuriously,
5601
5678
  // Enter always takes precedence.
@@ -5649,6 +5726,7 @@ class DOMObserver {
5649
5726
  let newSel = this.selectionChanged && hasSelection(this.dom, this.selectionRange);
5650
5727
  if (from < 0 && !newSel)
5651
5728
  return;
5729
+ this.view.inputState.lastFocusTime = 0;
5652
5730
  this.selectionChanged = false;
5653
5731
  let startState = this.view.state;
5654
5732
  let handled = this.onChange(from, to, typeOver);
@@ -6890,11 +6968,11 @@ function modifiers(name, event, shift) {
6890
6968
  name = "Shift-" + name;
6891
6969
  return name;
6892
6970
  }
6893
- const handleKeyEvents = EditorView.domEventHandlers({
6971
+ const handleKeyEvents = state.Prec.default(EditorView.domEventHandlers({
6894
6972
  keydown(event, view) {
6895
6973
  return runHandlers(getKeymap(view.state), event, view, "editor");
6896
6974
  }
6897
- });
6975
+ }));
6898
6976
  /**
6899
6977
  Facet used for registering keymaps.
6900
6978
 
@@ -7356,7 +7434,7 @@ function iterMatches(doc, re, from, to, f) {
7356
7434
  for (let cursor = doc.iterRange(from, to), pos = from, m; !cursor.next().done; pos += cursor.value.length) {
7357
7435
  if (!cursor.lineBreak)
7358
7436
  while (m = re.exec(cursor.value))
7359
- f(pos + m.index, pos + m.index + m[0].length, m);
7437
+ f(pos + m.index, m);
7360
7438
  }
7361
7439
  }
7362
7440
  function matchRanges(view, maxLength) {
@@ -7386,11 +7464,20 @@ class MatchDecorator {
7386
7464
  Create a decorator.
7387
7465
  */
7388
7466
  constructor(config) {
7389
- let { regexp, decoration, boundary, maxLength = 1000 } = config;
7467
+ const { regexp, decoration, decorate, boundary, maxLength = 1000 } = config;
7390
7468
  if (!regexp.global)
7391
7469
  throw new RangeError("The regular expression given to MatchDecorator should have its 'g' flag set");
7392
7470
  this.regexp = regexp;
7393
- this.getDeco = typeof decoration == "function" ? decoration : () => decoration;
7471
+ if (decorate) {
7472
+ this.addMatch = (match, view, from, add) => decorate(add, from, from + match[0].length, match, view);
7473
+ }
7474
+ else if (decoration) {
7475
+ let getDeco = typeof decoration == "function" ? decoration : () => decoration;
7476
+ this.addMatch = (match, view, from, add) => add(from, from + match[0].length, getDeco(match, view, from));
7477
+ }
7478
+ else {
7479
+ throw new RangeError("Either 'decorate' or 'decoration' should be provided to MatchDecorator");
7480
+ }
7394
7481
  this.boundary = boundary;
7395
7482
  this.maxLength = maxLength;
7396
7483
  }
@@ -7400,9 +7487,9 @@ class MatchDecorator {
7400
7487
  plugin.
7401
7488
  */
7402
7489
  createDeco(view) {
7403
- let build = new state.RangeSetBuilder();
7490
+ let build = new state.RangeSetBuilder(), add = build.add.bind(build);
7404
7491
  for (let { from, to } of matchRanges(view, this.maxLength))
7405
- iterMatches(view.state.doc, this.regexp, from, to, (a, b, m) => build.add(a, b, this.getDeco(m, view, a)));
7492
+ iterMatches(view.state.doc, this.regexp, from, to, (from, m) => this.addMatch(m, view, from, add));
7406
7493
  return build.finish();
7407
7494
  }
7408
7495
  /**
@@ -7444,15 +7531,14 @@ class MatchDecorator {
7444
7531
  }
7445
7532
  }
7446
7533
  let ranges = [], m;
7534
+ let add = (from, to, deco) => ranges.push(deco.range(from, to));
7447
7535
  if (fromLine == toLine) {
7448
7536
  this.regexp.lastIndex = start - fromLine.from;
7449
- while ((m = this.regexp.exec(fromLine.text)) && m.index < end - fromLine.from) {
7450
- let pos = m.index + fromLine.from;
7451
- ranges.push(this.getDeco(m, view, pos).range(pos, pos + m[0].length));
7452
- }
7537
+ while ((m = this.regexp.exec(fromLine.text)) && m.index < end - fromLine.from)
7538
+ this.addMatch(m, view, m.index + fromLine.from, add);
7453
7539
  }
7454
7540
  else {
7455
- iterMatches(view.state.doc, this.regexp, start, end, (from, to, m) => ranges.push(this.getDeco(m, view, from).range(from, to)));
7541
+ iterMatches(view.state.doc, this.regexp, start, end, (from, m) => this.addMatch(m, view, from, add));
7456
7542
  }
7457
7543
  deco = deco.update({ filterFrom: start, filterTo: end, filter: (from, to) => from < start || to > end, add: ranges });
7458
7544
  }
@@ -7462,7 +7548,7 @@ class MatchDecorator {
7462
7548
  }
7463
7549
 
7464
7550
  const UnicodeRegexpSupport = /x/.unicode != null ? "gu" : "g";
7465
- const Specials = new RegExp("[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\ufeff\ufff9-\ufffc]", UnicodeRegexpSupport);
7551
+ const Specials = new RegExp("[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc]", UnicodeRegexpSupport);
7466
7552
  const Names = {
7467
7553
  0: "null",
7468
7554
  7: "bell",
@@ -7479,6 +7565,9 @@ const Names = {
7479
7565
  8232: "line separator",
7480
7566
  8237: "left-to-right override",
7481
7567
  8238: "right-to-left override",
7568
+ 8294: "left-to-right isolate",
7569
+ 8295: "right-to-left isolate",
7570
+ 8297: "pop directional isolate",
7482
7571
  8233: "paragraph separator",
7483
7572
  65279: "zero width no-break space",
7484
7573
  65532: "object replacement"
package/dist/index.d.ts CHANGED
@@ -1357,7 +1357,7 @@ represent a matching configuration.
1357
1357
  */
1358
1358
  declare class MatchDecorator {
1359
1359
  private regexp;
1360
- private getDeco;
1360
+ private addMatch;
1361
1361
  private boundary;
1362
1362
  private maxLength;
1363
1363
  /**
@@ -1374,7 +1374,18 @@ declare class MatchDecorator {
1374
1374
  The decoration to apply to matches, either directly or as a
1375
1375
  function of the match.
1376
1376
  */
1377
- decoration: Decoration | ((match: RegExpExecArray, view: EditorView, pos: number) => Decoration);
1377
+ decoration?: Decoration | ((match: RegExpExecArray, view: EditorView, pos: number) => Decoration);
1378
+ /**
1379
+ Customize the way decorations are added for matches. This
1380
+ function, when given, will be called for matches and should
1381
+ call `add` to create decorations for them. Note that the
1382
+ decorations should appear *in* the given range, and the
1383
+ function should have no side effects beyond calling `add`.
1384
+
1385
+ The `decoration` option is ignored when `decorate` is
1386
+ provided.
1387
+ */
1388
+ decorate?: (add: (from: number, to: number, decoration: Decoration) => void, from: number, to: number, match: RegExpExecArray, view: EditorView) => void;
1378
1389
  /**
1379
1390
  By default, changed lines are re-matched entirely. You can
1380
1391
  provide a boundary expression, which should match single
package/dist/index.js CHANGED
@@ -264,6 +264,31 @@ function clearAttributes(node) {
264
264
  while (node.attributes.length)
265
265
  node.removeAttributeNode(node.attributes[0]);
266
266
  }
267
+ function atElementStart(doc, selection) {
268
+ let node = selection.focusNode, offset = selection.focusOffset;
269
+ if (!node || selection.anchorNode != node || selection.anchorOffset != offset)
270
+ return false;
271
+ for (;;) {
272
+ if (offset) {
273
+ if (node.nodeType != 1)
274
+ return false;
275
+ let prev = node.childNodes[offset - 1];
276
+ if (prev.contentEditable == "false")
277
+ offset--;
278
+ else {
279
+ node = prev;
280
+ offset = maxOffset(node);
281
+ }
282
+ }
283
+ else if (node == doc) {
284
+ return true;
285
+ }
286
+ else {
287
+ offset = domIndex(node);
288
+ node = node.parentNode;
289
+ }
290
+ }
291
+ }
267
292
 
268
293
  class DOMPos {
269
294
  constructor(node, offset, precise = true) {
@@ -3131,7 +3156,8 @@ function posAtCoords(view, { x, y }, precise, bias = -1) {
3131
3156
  let range = doc.caretRangeFromPoint(x, y);
3132
3157
  if (range) {
3133
3158
  ({ startContainer: node, startOffset: offset } = range);
3134
- if (browser.safari && isSuspiciousCaretResult(node, offset, x))
3159
+ if (browser.safari && isSuspiciousSafariCaretResult(node, offset, x) ||
3160
+ browser.chrome && isSuspiciousChromeCaretResult(node, offset, x))
3135
3161
  node = undefined;
3136
3162
  }
3137
3163
  }
@@ -3158,7 +3184,7 @@ function posAtCoordsImprecise(view, contentRect, block, x, y) {
3158
3184
  // the space between lines as belonging to the last character of the
3159
3185
  // line before. This is used to detect such a result so that it can be
3160
3186
  // ignored (issue #401).
3161
- function isSuspiciousCaretResult(node, offset, x) {
3187
+ function isSuspiciousSafariCaretResult(node, offset, x) {
3162
3188
  let len;
3163
3189
  if (node.nodeType != 3 || offset != (len = node.nodeValue.length))
3164
3190
  return false;
@@ -3167,6 +3193,22 @@ function isSuspiciousCaretResult(node, offset, x) {
3167
3193
  return false;
3168
3194
  return textRange(node, len - 1, len).getBoundingClientRect().left > x;
3169
3195
  }
3196
+ // Chrome will move positions between lines to the start of the next line
3197
+ function isSuspiciousChromeCaretResult(node, offset, x) {
3198
+ if (offset != 0)
3199
+ return false;
3200
+ for (let cur = node;;) {
3201
+ let parent = cur.parentNode;
3202
+ if (!parent || parent.nodeType != 1 || parent.firstChild != cur)
3203
+ return false;
3204
+ if (parent.classList.contains("cm-line"))
3205
+ break;
3206
+ cur = parent;
3207
+ }
3208
+ let rect = node.nodeType == 1 ? node.getBoundingClientRect()
3209
+ : textRange(node, 0, Math.max(node.nodeValue.length, 1)).getBoundingClientRect();
3210
+ return x - rect.left > 5;
3211
+ }
3170
3212
  function moveToLineBoundary(view, start, forward, includeWrap) {
3171
3213
  let line = view.state.doc.lineAt(start.head);
3172
3214
  let coords = !includeWrap || !view.lineWrapping ? null
@@ -3266,6 +3308,10 @@ class InputState {
3266
3308
  constructor(view) {
3267
3309
  this.lastKeyCode = 0;
3268
3310
  this.lastKeyTime = 0;
3311
+ this.lastTouchTime = 0;
3312
+ this.lastFocusTime = 0;
3313
+ this.lastScrollTop = 0;
3314
+ this.lastScrollLeft = 0;
3269
3315
  this.chromeScrollHack = -1;
3270
3316
  // On iOS, some keys need to have their default behavior happen
3271
3317
  // (after which we retroactively handle them and reset the DOM) to
@@ -3367,6 +3413,8 @@ class InputState {
3367
3413
  return false;
3368
3414
  }
3369
3415
  runScrollHandlers(view, event) {
3416
+ this.lastScrollTop = view.scrollDOM.scrollTop;
3417
+ this.lastScrollLeft = view.scrollDOM.scrollLeft;
3370
3418
  for (let set of this.customHandlers) {
3371
3419
  let handler = set.handlers.scroll;
3372
3420
  if (handler) {
@@ -3611,9 +3659,8 @@ handlers.keydown = (view, event) => {
3611
3659
  else if (modifierCodes.indexOf(event.keyCode) < 0)
3612
3660
  view.inputState.lastEscPress = 0;
3613
3661
  };
3614
- let lastTouch = 0;
3615
3662
  handlers.touchstart = (view, e) => {
3616
- lastTouch = Date.now();
3663
+ view.inputState.lastTouchTime = Date.now();
3617
3664
  view.inputState.setSelectionOrigin("select.pointer");
3618
3665
  };
3619
3666
  handlers.touchmove = view => {
@@ -3621,7 +3668,7 @@ handlers.touchmove = view => {
3621
3668
  };
3622
3669
  handlers.mousedown = (view, event) => {
3623
3670
  view.observer.flush();
3624
- if (lastTouch > Date.now() - 2000 && getClickType(event) == 1)
3671
+ if (view.inputState.lastTouchTime > Date.now() - 2000 && getClickType(event) == 1)
3625
3672
  return; // Ignore touch interaction
3626
3673
  let style = null;
3627
3674
  for (let makeStyle of view.state.facet(mouseSelectionStyle)) {
@@ -3725,6 +3772,8 @@ function basicMouseSelection(view, event) {
3725
3772
  }
3726
3773
  if (extend)
3727
3774
  return startSel.replaceRange(startSel.main.extend(range.from, range.to));
3775
+ else if (multiple && startSel.ranges.length > 1 && startSel.ranges.some(r => r.eq(range)))
3776
+ return removeRange(startSel, range);
3728
3777
  else if (multiple)
3729
3778
  return startSel.addRange(range);
3730
3779
  else
@@ -3732,6 +3781,12 @@ function basicMouseSelection(view, event) {
3732
3781
  }
3733
3782
  };
3734
3783
  }
3784
+ function removeRange(sel, range) {
3785
+ for (let i = 0;; i++) {
3786
+ if (sel.ranges[i].eq(range))
3787
+ return EditorSelection.create(sel.ranges.slice(0, i).concat(sel.ranges.slice(i + 1)), sel.mainIndex == i ? 0 : sel.mainIndex - (sel.mainIndex > i ? 1 : 0));
3788
+ }
3789
+ }
3735
3790
  handlers.dragstart = (view, event) => {
3736
3791
  let { selection: { main } } = view.state;
3737
3792
  let { mouseSelection } = view.inputState;
@@ -3867,7 +3922,15 @@ function updateForFocusChange(view) {
3867
3922
  view.update([]);
3868
3923
  }, 10);
3869
3924
  }
3870
- handlers.focus = updateForFocusChange;
3925
+ handlers.focus = view => {
3926
+ view.inputState.lastFocusTime = Date.now();
3927
+ // When focusing reset the scroll position, move it back to where it was
3928
+ if (!view.scrollDOM.scrollTop && (view.inputState.lastScrollTop || view.inputState.lastScrollLeft)) {
3929
+ view.scrollDOM.scrollTop = view.inputState.lastScrollTop;
3930
+ view.scrollDOM.scrollLeft = view.inputState.lastScrollLeft;
3931
+ }
3932
+ updateForFocusChange(view);
3933
+ };
3871
3934
  handlers.blur = view => {
3872
3935
  view.observer.clearSelectionRange();
3873
3936
  updateForFocusChange(view);
@@ -5215,8 +5278,8 @@ const baseTheme$1 = /*@__PURE__*/buildTheme("." + baseThemeID, {
5215
5278
  // Two animations defined so that we can switch between them to
5216
5279
  // restart the animation without forcing another style
5217
5280
  // recomputation.
5218
- "@keyframes cm-blink": { "0%": {}, "50%": { visibility: "hidden" }, "100%": {} },
5219
- "@keyframes cm-blink2": { "0%": {}, "50%": { visibility: "hidden" }, "100%": {} },
5281
+ "@keyframes cm-blink": { "0%": {}, "50%": { opacity: 0 }, "100%": {} },
5282
+ "@keyframes cm-blink2": { "0%": {}, "50%": { opacity: 0 }, "100%": {} },
5220
5283
  ".cm-cursor, .cm-dropCursor": {
5221
5284
  position: "absolute",
5222
5285
  borderLeft: "1.2px solid black",
@@ -5237,6 +5300,7 @@ const baseTheme$1 = /*@__PURE__*/buildTheme("." + baseThemeID, {
5237
5300
  "&light .cm-specialChar": { color: "red" },
5238
5301
  "&dark .cm-specialChar": { color: "#f78" },
5239
5302
  ".cm-gutters": {
5303
+ flexShrink: 0,
5240
5304
  display: "flex",
5241
5305
  height: "100%",
5242
5306
  boxSizing: "border-box",
@@ -5493,15 +5557,28 @@ class DOMObserver {
5493
5557
  this.flush(false);
5494
5558
  }
5495
5559
  readSelectionRange() {
5496
- let { root } = this.view;
5560
+ let { view } = this;
5497
5561
  // The Selection object is broken in shadow roots in Safari. See
5498
5562
  // https://github.com/codemirror/dev/issues/414
5499
- let range = browser.safari && root.nodeType == 11 && deepActiveElement() == this.view.contentDOM &&
5500
- safariSelectionRangeHack(this.view) || getSelection(root);
5563
+ let range = browser.safari && view.root.nodeType == 11 && deepActiveElement() == this.dom &&
5564
+ safariSelectionRangeHack(this.view) || getSelection(view.root);
5501
5565
  if (!range || this.selectionRange.eq(range))
5502
5566
  return false;
5567
+ let local = hasSelection(this.dom, range);
5568
+ // Detect the situation where the browser has, on focus, moved the
5569
+ // selection to the start of the content element. Reset it to the
5570
+ // position from the editor state.
5571
+ if (local && !this.selectionChanged && this.selectionRange.focusNode &&
5572
+ view.inputState.lastFocusTime > Date.now() - 200 &&
5573
+ view.inputState.lastTouchTime < Date.now() - 300 &&
5574
+ atElementStart(this.dom, range)) {
5575
+ view.docView.updateSelection();
5576
+ return false;
5577
+ }
5503
5578
  this.selectionRange.setRange(range);
5504
- return this.selectionChanged = true;
5579
+ if (local)
5580
+ this.selectionChanged = true;
5581
+ return true;
5505
5582
  }
5506
5583
  setSelectionRange(anchor, head) {
5507
5584
  this.selectionRange.set(anchor.node, anchor.offset, head.node, head.offset);
@@ -5588,7 +5665,7 @@ class DOMObserver {
5588
5665
  this.delayedAndroidKey = null;
5589
5666
  this.delayedFlush = -1;
5590
5667
  if (!this.flush())
5591
- dispatchKey(this.view.contentDOM, key.key, key.keyCode);
5668
+ dispatchKey(this.dom, key.key, key.keyCode);
5592
5669
  });
5593
5670
  // Since backspace beforeinput is sometimes signalled spuriously,
5594
5671
  // Enter always takes precedence.
@@ -5642,6 +5719,7 @@ class DOMObserver {
5642
5719
  let newSel = this.selectionChanged && hasSelection(this.dom, this.selectionRange);
5643
5720
  if (from < 0 && !newSel)
5644
5721
  return;
5722
+ this.view.inputState.lastFocusTime = 0;
5645
5723
  this.selectionChanged = false;
5646
5724
  let startState = this.view.state;
5647
5725
  let handled = this.onChange(from, to, typeOver);
@@ -6883,11 +6961,11 @@ function modifiers(name, event, shift) {
6883
6961
  name = "Shift-" + name;
6884
6962
  return name;
6885
6963
  }
6886
- const handleKeyEvents = /*@__PURE__*/EditorView.domEventHandlers({
6964
+ const handleKeyEvents = /*@__PURE__*/Prec.default(/*@__PURE__*/EditorView.domEventHandlers({
6887
6965
  keydown(event, view) {
6888
6966
  return runHandlers(getKeymap(view.state), event, view, "editor");
6889
6967
  }
6890
- });
6968
+ }));
6891
6969
  /**
6892
6970
  Facet used for registering keymaps.
6893
6971
 
@@ -7349,7 +7427,7 @@ function iterMatches(doc, re, from, to, f) {
7349
7427
  for (let cursor = doc.iterRange(from, to), pos = from, m; !cursor.next().done; pos += cursor.value.length) {
7350
7428
  if (!cursor.lineBreak)
7351
7429
  while (m = re.exec(cursor.value))
7352
- f(pos + m.index, pos + m.index + m[0].length, m);
7430
+ f(pos + m.index, m);
7353
7431
  }
7354
7432
  }
7355
7433
  function matchRanges(view, maxLength) {
@@ -7379,11 +7457,20 @@ class MatchDecorator {
7379
7457
  Create a decorator.
7380
7458
  */
7381
7459
  constructor(config) {
7382
- let { regexp, decoration, boundary, maxLength = 1000 } = config;
7460
+ const { regexp, decoration, decorate, boundary, maxLength = 1000 } = config;
7383
7461
  if (!regexp.global)
7384
7462
  throw new RangeError("The regular expression given to MatchDecorator should have its 'g' flag set");
7385
7463
  this.regexp = regexp;
7386
- this.getDeco = typeof decoration == "function" ? decoration : () => decoration;
7464
+ if (decorate) {
7465
+ this.addMatch = (match, view, from, add) => decorate(add, from, from + match[0].length, match, view);
7466
+ }
7467
+ else if (decoration) {
7468
+ let getDeco = typeof decoration == "function" ? decoration : () => decoration;
7469
+ this.addMatch = (match, view, from, add) => add(from, from + match[0].length, getDeco(match, view, from));
7470
+ }
7471
+ else {
7472
+ throw new RangeError("Either 'decorate' or 'decoration' should be provided to MatchDecorator");
7473
+ }
7387
7474
  this.boundary = boundary;
7388
7475
  this.maxLength = maxLength;
7389
7476
  }
@@ -7393,9 +7480,9 @@ class MatchDecorator {
7393
7480
  plugin.
7394
7481
  */
7395
7482
  createDeco(view) {
7396
- let build = new RangeSetBuilder();
7483
+ let build = new RangeSetBuilder(), add = build.add.bind(build);
7397
7484
  for (let { from, to } of matchRanges(view, this.maxLength))
7398
- iterMatches(view.state.doc, this.regexp, from, to, (a, b, m) => build.add(a, b, this.getDeco(m, view, a)));
7485
+ iterMatches(view.state.doc, this.regexp, from, to, (from, m) => this.addMatch(m, view, from, add));
7399
7486
  return build.finish();
7400
7487
  }
7401
7488
  /**
@@ -7437,15 +7524,14 @@ class MatchDecorator {
7437
7524
  }
7438
7525
  }
7439
7526
  let ranges = [], m;
7527
+ let add = (from, to, deco) => ranges.push(deco.range(from, to));
7440
7528
  if (fromLine == toLine) {
7441
7529
  this.regexp.lastIndex = start - fromLine.from;
7442
- while ((m = this.regexp.exec(fromLine.text)) && m.index < end - fromLine.from) {
7443
- let pos = m.index + fromLine.from;
7444
- ranges.push(this.getDeco(m, view, pos).range(pos, pos + m[0].length));
7445
- }
7530
+ while ((m = this.regexp.exec(fromLine.text)) && m.index < end - fromLine.from)
7531
+ this.addMatch(m, view, m.index + fromLine.from, add);
7446
7532
  }
7447
7533
  else {
7448
- iterMatches(view.state.doc, this.regexp, start, end, (from, to, m) => ranges.push(this.getDeco(m, view, from).range(from, to)));
7534
+ iterMatches(view.state.doc, this.regexp, start, end, (from, m) => this.addMatch(m, view, from, add));
7449
7535
  }
7450
7536
  deco = deco.update({ filterFrom: start, filterTo: end, filter: (from, to) => from < start || to > end, add: ranges });
7451
7537
  }
@@ -7455,7 +7541,7 @@ class MatchDecorator {
7455
7541
  }
7456
7542
 
7457
7543
  const UnicodeRegexpSupport = /x/.unicode != null ? "gu" : "g";
7458
- const Specials = /*@__PURE__*/new RegExp("[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\ufeff\ufff9-\ufffc]", UnicodeRegexpSupport);
7544
+ const Specials = /*@__PURE__*/new RegExp("[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc]", UnicodeRegexpSupport);
7459
7545
  const Names = {
7460
7546
  0: "null",
7461
7547
  7: "bell",
@@ -7472,6 +7558,9 @@ const Names = {
7472
7558
  8232: "line separator",
7473
7559
  8237: "left-to-right override",
7474
7560
  8238: "right-to-left override",
7561
+ 8294: "left-to-right isolate",
7562
+ 8295: "right-to-left isolate",
7563
+ 8297: "pop directional isolate",
7475
7564
  8233: "paragraph separator",
7476
7565
  65279: "zero width no-break space",
7477
7566
  65532: "object replacement"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.0.2",
3
+ "version": "6.1.1",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",