@codemirror/view 6.38.5 → 6.38.7

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,19 @@
1
+ ## 6.38.7 (2025-11-14)
2
+
3
+ ### Bug fixes
4
+
5
+ Make detection of transformed tooltip parent elements (forcing absolute positioning) more robust on current browsers.
6
+
7
+ 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.
8
+
9
+ 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.
10
+
11
+ ## 6.38.6 (2025-10-13)
12
+
13
+ ### Bug fixes
14
+
15
+ Work around a regression in Safari 26 that causes fragments of old selections to remain visible.
16
+
1
17
  ## 6.38.5 (2025-10-07)
2
18
 
3
19
  ### 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 &&
@@ -4069,6 +4076,18 @@ function applyDOMChange(view, domChange) {
4069
4076
  insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
4070
4077
  };
4071
4078
  }
4079
+ else if (view.state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
4080
+ view.inputState.insertingTextAt > Date.now() - 50) {
4081
+ // For a cross-line insertion, Chrome and Safari will crudely take
4082
+ // the text of the line after the selection, flattening any
4083
+ // widgets, and move it into the joined line. This tries to detect
4084
+ // such a situation, and replaces the change with a selection
4085
+ // replace of the text provided by the beforeinput event.
4086
+ change = {
4087
+ from: sel.from, to: sel.to,
4088
+ insert: view.state.toText(view.inputState.insertingText)
4089
+ };
4090
+ }
4072
4091
  else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
4073
4092
  change.insert.toString() == "\n " && view.lineWrapping) {
4074
4093
  // In Chrome, if you insert a space at the start of a wrapped
@@ -4155,7 +4174,7 @@ function applyDefaultInsert(view, change, newSel) {
4155
4174
  let mainSel = newSel && newSel.main.to <= changes.newLength ? newSel.main : undefined;
4156
4175
  // Try to apply a composition change to all cursors
4157
4176
  if (startState.selection.ranges.length > 1 && view.inputState.composing >= 0 &&
4158
- change.to <= sel.to && change.to >= sel.to - 10) {
4177
+ change.to <= sel.to + 10 && change.to >= sel.to - 10) {
4159
4178
  let replaced = view.state.sliceDoc(change.from, change.to);
4160
4179
  let compositionRange, composition = newSel && findCompositionNode(view, newSel.main.head);
4161
4180
  if (composition) {
@@ -4302,6 +4321,9 @@ class InputState {
4302
4321
  // Used to categorize changes as part of a composition, even when
4303
4322
  // the mutation events fire shortly after the compositionend event
4304
4323
  this.compositionPendingChange = false;
4324
+ // Set by beforeinput, used in DOM change reader
4325
+ this.insertingText = "";
4326
+ this.insertingTextAt = 0;
4305
4327
  this.mouseSelection = null;
4306
4328
  // When a drag from the editor is active, this points at the range
4307
4329
  // being dragged.
@@ -5056,6 +5078,10 @@ observers.contextmenu = view => {
5056
5078
  };
5057
5079
  handlers.beforeinput = (view, event) => {
5058
5080
  var _a, _b;
5081
+ if (event.inputType == "insertText" || event.inputType == "insertCompositionText") {
5082
+ view.inputState.insertingText = event.data;
5083
+ view.inputState.insertingTextAt = Date.now();
5084
+ }
5059
5085
  // In EditContext mode, we must handle insertReplacementText events
5060
5086
  // directly, to make spell checking corrections work
5061
5087
  if (event.inputType == "insertReplacementText" && view.observer.editContext) {
@@ -8562,7 +8588,7 @@ Facet that works much like
8562
8588
  [`decorations`](https://codemirror.net/6/docs/ref/#view.EditorView^decorations), but puts its
8563
8589
  inputs at the very bottom of the precedence stack, meaning mark
8564
8590
  decorations provided here will only be split by other, partially
8565
- overlapping \`outerDecorations\` ranges, and wrap around all
8591
+ overlapping `outerDecorations` ranges, and wrap around all
8566
8592
  regular decorations. Use this for mark elements that should, as
8567
8593
  much as possible, remain in one piece.
8568
8594
  */
@@ -9130,7 +9156,7 @@ class LayerView {
9130
9156
  old = next;
9131
9157
  }
9132
9158
  this.drawn = markers;
9133
- if (browser.ios) // Issue #1600
9159
+ if (browser.safari && browser.safari_version >= 26) // Issue #1600, 1627
9134
9160
  this.dom.style.display = this.dom.firstChild ? "" : "none";
9135
9161
  }
9136
9162
  }
@@ -10060,18 +10086,18 @@ const tooltipPlugin = ViewPlugin.fromClass(class {
10060
10086
  let scaleX = 1, scaleY = 1, makeAbsolute = false;
10061
10087
  if (this.position == "fixed" && this.manager.tooltipViews.length) {
10062
10088
  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.
10089
+ if (browser.safari) {
10090
+ // Safari always sets offsetParent to null, even if a fixed
10091
+ // element is positioned relative to a transformed parent. So
10092
+ // we use this kludge to try and detect this.
10072
10093
  let rect = dom.getBoundingClientRect();
10073
10094
  makeAbsolute = Math.abs(rect.top + 10000) > 1 || Math.abs(rect.left) > 1;
10074
10095
  }
10096
+ else {
10097
+ // More conforming browsers will set offsetParent to the
10098
+ // transformed element.
10099
+ makeAbsolute = !!dom.offsetParent && dom.offsetParent != this.container.ownerDocument.body;
10100
+ }
10075
10101
  }
10076
10102
  if (makeAbsolute || this.position == "absolute") {
10077
10103
  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 &&
@@ -4065,6 +4072,18 @@ function applyDOMChange(view, domChange) {
4065
4072
  insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
4066
4073
  };
4067
4074
  }
4075
+ else if (view.state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
4076
+ view.inputState.insertingTextAt > Date.now() - 50) {
4077
+ // For a cross-line insertion, Chrome and Safari will crudely take
4078
+ // the text of the line after the selection, flattening any
4079
+ // widgets, and move it into the joined line. This tries to detect
4080
+ // such a situation, and replaces the change with a selection
4081
+ // replace of the text provided by the beforeinput event.
4082
+ change = {
4083
+ from: sel.from, to: sel.to,
4084
+ insert: view.state.toText(view.inputState.insertingText)
4085
+ };
4086
+ }
4068
4087
  else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
4069
4088
  change.insert.toString() == "\n " && view.lineWrapping) {
4070
4089
  // In Chrome, if you insert a space at the start of a wrapped
@@ -4151,7 +4170,7 @@ function applyDefaultInsert(view, change, newSel) {
4151
4170
  let mainSel = newSel && newSel.main.to <= changes.newLength ? newSel.main : undefined;
4152
4171
  // Try to apply a composition change to all cursors
4153
4172
  if (startState.selection.ranges.length > 1 && view.inputState.composing >= 0 &&
4154
- change.to <= sel.to && change.to >= sel.to - 10) {
4173
+ change.to <= sel.to + 10 && change.to >= sel.to - 10) {
4155
4174
  let replaced = view.state.sliceDoc(change.from, change.to);
4156
4175
  let compositionRange, composition = newSel && findCompositionNode(view, newSel.main.head);
4157
4176
  if (composition) {
@@ -4298,6 +4317,9 @@ class InputState {
4298
4317
  // Used to categorize changes as part of a composition, even when
4299
4318
  // the mutation events fire shortly after the compositionend event
4300
4319
  this.compositionPendingChange = false;
4320
+ // Set by beforeinput, used in DOM change reader
4321
+ this.insertingText = "";
4322
+ this.insertingTextAt = 0;
4301
4323
  this.mouseSelection = null;
4302
4324
  // When a drag from the editor is active, this points at the range
4303
4325
  // being dragged.
@@ -5052,6 +5074,10 @@ observers.contextmenu = view => {
5052
5074
  };
5053
5075
  handlers.beforeinput = (view, event) => {
5054
5076
  var _a, _b;
5077
+ if (event.inputType == "insertText" || event.inputType == "insertCompositionText") {
5078
+ view.inputState.insertingText = event.data;
5079
+ view.inputState.insertingTextAt = Date.now();
5080
+ }
5055
5081
  // In EditContext mode, we must handle insertReplacementText events
5056
5082
  // directly, to make spell checking corrections work
5057
5083
  if (event.inputType == "insertReplacementText" && view.observer.editContext) {
@@ -8557,7 +8583,7 @@ Facet that works much like
8557
8583
  [`decorations`](https://codemirror.net/6/docs/ref/#view.EditorView^decorations), but puts its
8558
8584
  inputs at the very bottom of the precedence stack, meaning mark
8559
8585
  decorations provided here will only be split by other, partially
8560
- overlapping \`outerDecorations\` ranges, and wrap around all
8586
+ overlapping `outerDecorations` ranges, and wrap around all
8561
8587
  regular decorations. Use this for mark elements that should, as
8562
8588
  much as possible, remain in one piece.
8563
8589
  */
@@ -9125,7 +9151,7 @@ class LayerView {
9125
9151
  old = next;
9126
9152
  }
9127
9153
  this.drawn = markers;
9128
- if (browser.ios) // Issue #1600
9154
+ if (browser.safari && browser.safari_version >= 26) // Issue #1600, 1627
9129
9155
  this.dom.style.display = this.dom.firstChild ? "" : "none";
9130
9156
  }
9131
9157
  }
@@ -10055,18 +10081,18 @@ const tooltipPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
10055
10081
  let scaleX = 1, scaleY = 1, makeAbsolute = false;
10056
10082
  if (this.position == "fixed" && this.manager.tooltipViews.length) {
10057
10083
  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.
10084
+ if (browser.safari) {
10085
+ // Safari always sets offsetParent to null, even if a fixed
10086
+ // element is positioned relative to a transformed parent. So
10087
+ // we use this kludge to try and detect this.
10067
10088
  let rect = dom.getBoundingClientRect();
10068
10089
  makeAbsolute = Math.abs(rect.top + 10000) > 1 || Math.abs(rect.left) > 1;
10069
10090
  }
10091
+ else {
10092
+ // More conforming browsers will set offsetParent to the
10093
+ // transformed element.
10094
+ makeAbsolute = !!dom.offsetParent && dom.offsetParent != this.container.ownerDocument.body;
10095
+ }
10070
10096
  }
10071
10097
  if (makeAbsolute || this.position == "absolute") {
10072
10098
  if (this.parent) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.38.5",
3
+ "version": "6.38.7",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",