@codemirror/view 6.38.6 → 6.38.8

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,21 @@
1
+ ## 6.38.8 (2025-11-17)
2
+
3
+ ### Bug fixes
4
+
5
+ Improve handling of composition with multiple cursors on MacOS.
6
+
7
+ Fix an issue where computing a document position from screen coordinates would sometimes go wrong in right-to-left text.
8
+
9
+ ## 6.38.7 (2025-11-14)
10
+
11
+ ### Bug fixes
12
+
13
+ Make detection of transformed tooltip parent elements (forcing absolute positioning) more robust on current browsers.
14
+
15
+ Avoid an issue where on Chrome and Safari, typing over a cross-line selection can replace widgets on the line after the selection with their plain text content.
16
+
17
+ Fix a bug that broke insertion of composed input at multiple cursors when the IME keeps the selection at the start of the composed text.
18
+
1
19
  ## 6.38.6 (2025-10-13)
2
20
 
3
21
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -3324,6 +3324,13 @@ class DocView extends ContentView {
3324
3324
  let { offsetWidth, offsetHeight } = this.view.scrollDOM;
3325
3325
  scrollRectIntoView(this.view.scrollDOM, targetRect, range.head < range.anchor ? -1 : 1, target.x, target.y, Math.max(Math.min(target.xMargin, offsetWidth), -offsetWidth), Math.max(Math.min(target.yMargin, offsetHeight), -offsetHeight), this.view.textDirection == exports.Direction.LTR);
3326
3326
  }
3327
+ lineHasWidget(pos) {
3328
+ let { i } = this.childCursor().findPos(pos);
3329
+ if (i == this.children.length)
3330
+ return false;
3331
+ let scan = (child) => child instanceof WidgetView || child.children.some(scan);
3332
+ return scan(this.children[i]);
3333
+ }
3327
3334
  }
3328
3335
  function betweenUneditable(pos) {
3329
3336
  return pos.node.nodeType == 1 && pos.node.firstChild &&
@@ -3541,7 +3548,7 @@ function domPosInText(node, x, y) {
3541
3548
  // Check for RTL on browsers that support getting client
3542
3549
  // rects for empty ranges.
3543
3550
  let rectBefore = textRange(node, i).getBoundingClientRect();
3544
- if (rectBefore.left == rect.right)
3551
+ if (Math.abs(rectBefore.left - rect.right) < 0.1)
3545
3552
  after = !right;
3546
3553
  }
3547
3554
  if (dy <= 0)
@@ -4013,7 +4020,10 @@ class DOMChange {
4013
4020
  anchor = view.state.doc.length;
4014
4021
  }
4015
4022
  }
4016
- this.newSel = state.EditorSelection.single(anchor, head);
4023
+ if (view.inputState.composing > -1 && view.state.selection.ranges.length > 1)
4024
+ this.newSel = view.state.selection.replaceRange(state.EditorSelection.range(anchor, head));
4025
+ else
4026
+ this.newSel = state.EditorSelection.single(anchor, head);
4017
4027
  }
4018
4028
  }
4019
4029
  }
@@ -4069,6 +4079,18 @@ function applyDOMChange(view, domChange) {
4069
4079
  insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
4070
4080
  };
4071
4081
  }
4082
+ else if (view.state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
4083
+ view.inputState.insertingTextAt > Date.now() - 50) {
4084
+ // For a cross-line insertion, Chrome and Safari will crudely take
4085
+ // the text of the line after the selection, flattening any
4086
+ // widgets, and move it into the joined line. This tries to detect
4087
+ // such a situation, and replaces the change with a selection
4088
+ // replace of the text provided by the beforeinput event.
4089
+ change = {
4090
+ from: sel.from, to: sel.to,
4091
+ insert: view.state.toText(view.inputState.insertingText)
4092
+ };
4093
+ }
4072
4094
  else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
4073
4095
  change.insert.toString() == "\n " && view.lineWrapping) {
4074
4096
  // In Chrome, if you insert a space at the start of a wrapped
@@ -4154,8 +4176,8 @@ function applyDefaultInsert(view, change, newSel) {
4154
4176
  let changes = startState.changes(change);
4155
4177
  let mainSel = newSel && newSel.main.to <= changes.newLength ? newSel.main : undefined;
4156
4178
  // Try to apply a composition change to all cursors
4157
- if (startState.selection.ranges.length > 1 && view.inputState.composing >= 0 &&
4158
- change.to <= sel.to && change.to >= sel.to - 10) {
4179
+ if (startState.selection.ranges.length > 1 && (view.inputState.composing >= 0 || view.inputState.compositionPendingChange) &&
4180
+ change.to <= sel.to + 10 && change.to >= sel.to - 10) {
4159
4181
  let replaced = view.state.sliceDoc(change.from, change.to);
4160
4182
  let compositionRange, composition = newSel && findCompositionNode(view, newSel.main.head);
4161
4183
  if (composition) {
@@ -4165,17 +4187,17 @@ function applyDefaultInsert(view, change, newSel) {
4165
4187
  else {
4166
4188
  compositionRange = view.state.doc.lineAt(sel.head);
4167
4189
  }
4168
- let offset = sel.to - change.to, size = sel.to - sel.from;
4190
+ let offset = sel.to - change.to;
4169
4191
  tr = startState.changeByRange(range => {
4170
4192
  if (range.from == sel.from && range.to == sel.to)
4171
4193
  return { changes, range: mainSel || range.map(changes) };
4172
4194
  let to = range.to - offset, from = to - replaced.length;
4173
- if (range.to - range.from != size || view.state.sliceDoc(from, to) != replaced ||
4195
+ if (view.state.sliceDoc(from, to) != replaced ||
4174
4196
  // Unfortunately, there's no way to make multiple
4175
4197
  // changes in the same node work without aborting
4176
4198
  // composition, so cursors in the composition range are
4177
4199
  // ignored.
4178
- range.to >= compositionRange.from && range.from <= compositionRange.to)
4200
+ to >= compositionRange.from && from <= compositionRange.to)
4179
4201
  return { range };
4180
4202
  let rangeChanges = startState.changes({ from, to, insert: change.insert }), selOff = range.to - sel.to;
4181
4203
  return {
@@ -4302,6 +4324,9 @@ class InputState {
4302
4324
  // Used to categorize changes as part of a composition, even when
4303
4325
  // the mutation events fire shortly after the compositionend event
4304
4326
  this.compositionPendingChange = false;
4327
+ // Set by beforeinput, used in DOM change reader
4328
+ this.insertingText = "";
4329
+ this.insertingTextAt = 0;
4305
4330
  this.mouseSelection = null;
4306
4331
  // When a drag from the editor is active, this points at the range
4307
4332
  // being dragged.
@@ -5056,6 +5081,10 @@ observers.contextmenu = view => {
5056
5081
  };
5057
5082
  handlers.beforeinput = (view, event) => {
5058
5083
  var _a, _b;
5084
+ if (event.inputType == "insertText" || event.inputType == "insertCompositionText") {
5085
+ view.inputState.insertingText = event.data;
5086
+ view.inputState.insertingTextAt = Date.now();
5087
+ }
5059
5088
  // In EditContext mode, we must handle insertReplacementText events
5060
5089
  // directly, to make spell checking corrections work
5061
5090
  if (event.inputType == "insertReplacementText" && view.observer.editContext) {
@@ -8562,7 +8591,7 @@ Facet that works much like
8562
8591
  [`decorations`](https://codemirror.net/6/docs/ref/#view.EditorView^decorations), but puts its
8563
8592
  inputs at the very bottom of the precedence stack, meaning mark
8564
8593
  decorations provided here will only be split by other, partially
8565
- overlapping \`outerDecorations\` ranges, and wrap around all
8594
+ overlapping `outerDecorations` ranges, and wrap around all
8566
8595
  regular decorations. Use this for mark elements that should, as
8567
8596
  much as possible, remain in one piece.
8568
8597
  */
@@ -10060,18 +10089,18 @@ const tooltipPlugin = ViewPlugin.fromClass(class {
10060
10089
  let scaleX = 1, scaleY = 1, makeAbsolute = false;
10061
10090
  if (this.position == "fixed" && this.manager.tooltipViews.length) {
10062
10091
  let { dom } = this.manager.tooltipViews[0];
10063
- if (browser.gecko) {
10064
- // Firefox sets the element's `offsetParent` to the
10065
- // transformed element when a transform interferes with fixed
10066
- // positioning.
10067
- makeAbsolute = dom.offsetParent != this.container.ownerDocument.body;
10068
- }
10069
- else if (dom.style.top == Outside && dom.style.left == "0px") {
10070
- // On other browsers, we have to awkwardly try and use other
10071
- // information to detect a transform.
10092
+ if (browser.safari) {
10093
+ // Safari always sets offsetParent to null, even if a fixed
10094
+ // element is positioned relative to a transformed parent. So
10095
+ // we use this kludge to try and detect this.
10072
10096
  let rect = dom.getBoundingClientRect();
10073
10097
  makeAbsolute = Math.abs(rect.top + 10000) > 1 || Math.abs(rect.left) > 1;
10074
10098
  }
10099
+ else {
10100
+ // More conforming browsers will set offsetParent to the
10101
+ // transformed element.
10102
+ makeAbsolute = !!dom.offsetParent && dom.offsetParent != this.container.ownerDocument.body;
10103
+ }
10075
10104
  }
10076
10105
  if (makeAbsolute || this.position == "absolute") {
10077
10106
  if (this.parent) {
package/dist/index.d.cts CHANGED
@@ -1265,7 +1265,7 @@ declare class EditorView {
1265
1265
  [`decorations`](https://codemirror.net/6/docs/ref/#view.EditorView^decorations), but puts its
1266
1266
  inputs at the very bottom of the precedence stack, meaning mark
1267
1267
  decorations provided here will only be split by other, partially
1268
- overlapping \`outerDecorations\` ranges, and wrap around all
1268
+ overlapping `outerDecorations` ranges, and wrap around all
1269
1269
  regular decorations. Use this for mark elements that should, as
1270
1270
  much as possible, remain in one piece.
1271
1271
  */
@@ -2091,8 +2091,8 @@ declare const showPanel: Facet<PanelConstructor | null, readonly (PanelConstruct
2091
2091
  type DialogConfig = {
2092
2092
  /**
2093
2093
  A function to render the content of the dialog. The result
2094
- should contain at least one `<form>` element. Submit handlers a
2095
- handler for the Escape key will be added to the form.
2094
+ should contain at least one `<form>` element. Submit handlers
2095
+ and a handler for the Escape key will be added to the form.
2096
2096
 
2097
2097
  If this is not given, the `label`, `input`, and `submitLabel`
2098
2098
  fields will be used to create a simple form for you.
package/dist/index.d.ts CHANGED
@@ -1265,7 +1265,7 @@ declare class EditorView {
1265
1265
  [`decorations`](https://codemirror.net/6/docs/ref/#view.EditorView^decorations), but puts its
1266
1266
  inputs at the very bottom of the precedence stack, meaning mark
1267
1267
  decorations provided here will only be split by other, partially
1268
- overlapping \`outerDecorations\` ranges, and wrap around all
1268
+ overlapping `outerDecorations` ranges, and wrap around all
1269
1269
  regular decorations. Use this for mark elements that should, as
1270
1270
  much as possible, remain in one piece.
1271
1271
  */
@@ -2091,8 +2091,8 @@ declare const showPanel: Facet<PanelConstructor | null, readonly (PanelConstruct
2091
2091
  type DialogConfig = {
2092
2092
  /**
2093
2093
  A function to render the content of the dialog. The result
2094
- should contain at least one `<form>` element. Submit handlers a
2095
- handler for the Escape key will be added to the form.
2094
+ should contain at least one `<form>` element. Submit handlers
2095
+ and a handler for the Escape key will be added to the form.
2096
2096
 
2097
2097
  If this is not given, the `label`, `input`, and `submitLabel`
2098
2098
  fields will be used to create a simple form for you.
package/dist/index.js CHANGED
@@ -3320,6 +3320,13 @@ class DocView extends ContentView {
3320
3320
  let { offsetWidth, offsetHeight } = this.view.scrollDOM;
3321
3321
  scrollRectIntoView(this.view.scrollDOM, targetRect, range.head < range.anchor ? -1 : 1, target.x, target.y, Math.max(Math.min(target.xMargin, offsetWidth), -offsetWidth), Math.max(Math.min(target.yMargin, offsetHeight), -offsetHeight), this.view.textDirection == Direction.LTR);
3322
3322
  }
3323
+ lineHasWidget(pos) {
3324
+ let { i } = this.childCursor().findPos(pos);
3325
+ if (i == this.children.length)
3326
+ return false;
3327
+ let scan = (child) => child instanceof WidgetView || child.children.some(scan);
3328
+ return scan(this.children[i]);
3329
+ }
3323
3330
  }
3324
3331
  function betweenUneditable(pos) {
3325
3332
  return pos.node.nodeType == 1 && pos.node.firstChild &&
@@ -3537,7 +3544,7 @@ function domPosInText(node, x, y) {
3537
3544
  // Check for RTL on browsers that support getting client
3538
3545
  // rects for empty ranges.
3539
3546
  let rectBefore = textRange(node, i).getBoundingClientRect();
3540
- if (rectBefore.left == rect.right)
3547
+ if (Math.abs(rectBefore.left - rect.right) < 0.1)
3541
3548
  after = !right;
3542
3549
  }
3543
3550
  if (dy <= 0)
@@ -4009,7 +4016,10 @@ class DOMChange {
4009
4016
  anchor = view.state.doc.length;
4010
4017
  }
4011
4018
  }
4012
- this.newSel = EditorSelection.single(anchor, head);
4019
+ if (view.inputState.composing > -1 && view.state.selection.ranges.length > 1)
4020
+ this.newSel = view.state.selection.replaceRange(EditorSelection.range(anchor, head));
4021
+ else
4022
+ this.newSel = EditorSelection.single(anchor, head);
4013
4023
  }
4014
4024
  }
4015
4025
  }
@@ -4065,6 +4075,18 @@ function applyDOMChange(view, domChange) {
4065
4075
  insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
4066
4076
  };
4067
4077
  }
4078
+ else if (view.state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
4079
+ view.inputState.insertingTextAt > Date.now() - 50) {
4080
+ // For a cross-line insertion, Chrome and Safari will crudely take
4081
+ // the text of the line after the selection, flattening any
4082
+ // widgets, and move it into the joined line. This tries to detect
4083
+ // such a situation, and replaces the change with a selection
4084
+ // replace of the text provided by the beforeinput event.
4085
+ change = {
4086
+ from: sel.from, to: sel.to,
4087
+ insert: view.state.toText(view.inputState.insertingText)
4088
+ };
4089
+ }
4068
4090
  else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
4069
4091
  change.insert.toString() == "\n " && view.lineWrapping) {
4070
4092
  // In Chrome, if you insert a space at the start of a wrapped
@@ -4150,8 +4172,8 @@ function applyDefaultInsert(view, change, newSel) {
4150
4172
  let changes = startState.changes(change);
4151
4173
  let mainSel = newSel && newSel.main.to <= changes.newLength ? newSel.main : undefined;
4152
4174
  // Try to apply a composition change to all cursors
4153
- if (startState.selection.ranges.length > 1 && view.inputState.composing >= 0 &&
4154
- change.to <= sel.to && change.to >= sel.to - 10) {
4175
+ if (startState.selection.ranges.length > 1 && (view.inputState.composing >= 0 || view.inputState.compositionPendingChange) &&
4176
+ change.to <= sel.to + 10 && change.to >= sel.to - 10) {
4155
4177
  let replaced = view.state.sliceDoc(change.from, change.to);
4156
4178
  let compositionRange, composition = newSel && findCompositionNode(view, newSel.main.head);
4157
4179
  if (composition) {
@@ -4161,17 +4183,17 @@ function applyDefaultInsert(view, change, newSel) {
4161
4183
  else {
4162
4184
  compositionRange = view.state.doc.lineAt(sel.head);
4163
4185
  }
4164
- let offset = sel.to - change.to, size = sel.to - sel.from;
4186
+ let offset = sel.to - change.to;
4165
4187
  tr = startState.changeByRange(range => {
4166
4188
  if (range.from == sel.from && range.to == sel.to)
4167
4189
  return { changes, range: mainSel || range.map(changes) };
4168
4190
  let to = range.to - offset, from = to - replaced.length;
4169
- if (range.to - range.from != size || view.state.sliceDoc(from, to) != replaced ||
4191
+ if (view.state.sliceDoc(from, to) != replaced ||
4170
4192
  // Unfortunately, there's no way to make multiple
4171
4193
  // changes in the same node work without aborting
4172
4194
  // composition, so cursors in the composition range are
4173
4195
  // ignored.
4174
- range.to >= compositionRange.from && range.from <= compositionRange.to)
4196
+ to >= compositionRange.from && from <= compositionRange.to)
4175
4197
  return { range };
4176
4198
  let rangeChanges = startState.changes({ from, to, insert: change.insert }), selOff = range.to - sel.to;
4177
4199
  return {
@@ -4298,6 +4320,9 @@ class InputState {
4298
4320
  // Used to categorize changes as part of a composition, even when
4299
4321
  // the mutation events fire shortly after the compositionend event
4300
4322
  this.compositionPendingChange = false;
4323
+ // Set by beforeinput, used in DOM change reader
4324
+ this.insertingText = "";
4325
+ this.insertingTextAt = 0;
4301
4326
  this.mouseSelection = null;
4302
4327
  // When a drag from the editor is active, this points at the range
4303
4328
  // being dragged.
@@ -5052,6 +5077,10 @@ observers.contextmenu = view => {
5052
5077
  };
5053
5078
  handlers.beforeinput = (view, event) => {
5054
5079
  var _a, _b;
5080
+ if (event.inputType == "insertText" || event.inputType == "insertCompositionText") {
5081
+ view.inputState.insertingText = event.data;
5082
+ view.inputState.insertingTextAt = Date.now();
5083
+ }
5055
5084
  // In EditContext mode, we must handle insertReplacementText events
5056
5085
  // directly, to make spell checking corrections work
5057
5086
  if (event.inputType == "insertReplacementText" && view.observer.editContext) {
@@ -8557,7 +8586,7 @@ Facet that works much like
8557
8586
  [`decorations`](https://codemirror.net/6/docs/ref/#view.EditorView^decorations), but puts its
8558
8587
  inputs at the very bottom of the precedence stack, meaning mark
8559
8588
  decorations provided here will only be split by other, partially
8560
- overlapping \`outerDecorations\` ranges, and wrap around all
8589
+ overlapping `outerDecorations` ranges, and wrap around all
8561
8590
  regular decorations. Use this for mark elements that should, as
8562
8591
  much as possible, remain in one piece.
8563
8592
  */
@@ -10055,18 +10084,18 @@ const tooltipPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
10055
10084
  let scaleX = 1, scaleY = 1, makeAbsolute = false;
10056
10085
  if (this.position == "fixed" && this.manager.tooltipViews.length) {
10057
10086
  let { dom } = this.manager.tooltipViews[0];
10058
- if (browser.gecko) {
10059
- // Firefox sets the element's `offsetParent` to the
10060
- // transformed element when a transform interferes with fixed
10061
- // positioning.
10062
- makeAbsolute = dom.offsetParent != this.container.ownerDocument.body;
10063
- }
10064
- else if (dom.style.top == Outside && dom.style.left == "0px") {
10065
- // On other browsers, we have to awkwardly try and use other
10066
- // information to detect a transform.
10087
+ if (browser.safari) {
10088
+ // Safari always sets offsetParent to null, even if a fixed
10089
+ // element is positioned relative to a transformed parent. So
10090
+ // we use this kludge to try and detect this.
10067
10091
  let rect = dom.getBoundingClientRect();
10068
10092
  makeAbsolute = Math.abs(rect.top + 10000) > 1 || Math.abs(rect.left) > 1;
10069
10093
  }
10094
+ else {
10095
+ // More conforming browsers will set offsetParent to the
10096
+ // transformed element.
10097
+ makeAbsolute = !!dom.offsetParent && dom.offsetParent != this.container.ownerDocument.body;
10098
+ }
10070
10099
  }
10071
10100
  if (makeAbsolute || this.position == "absolute") {
10072
10101
  if (this.parent) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.38.6",
3
+ "version": "6.38.8",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",