@codemirror/view 6.39.15 → 6.39.17
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 +20 -0
- package/dist/index.cjs +134 -65
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +134 -65
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## 6.39.17 (2026-03-10)
|
|
2
|
+
|
|
3
|
+
### Bug fixes
|
|
4
|
+
|
|
5
|
+
Improve touch tap-selection on line wrapping boundaries.
|
|
6
|
+
|
|
7
|
+
Make `drawSelection` draw our own selection handles on iOS.
|
|
8
|
+
|
|
9
|
+
Fix an issue where `posAtCoords`, when querying line wrapping points, got confused by extra empty client rectangles produced by Safari.
|
|
10
|
+
|
|
11
|
+
## 6.39.16 (2026-03-02)
|
|
12
|
+
|
|
13
|
+
### Bug fixes
|
|
14
|
+
|
|
15
|
+
Perform scroll stabilization on the document or wrapping scrollable elements, when the user scrolls the editor.
|
|
16
|
+
|
|
17
|
+
Fix an issue where changing decorations right before a composition could end up corrupting the visible DOM.
|
|
18
|
+
|
|
19
|
+
Fix an issue where some types of text input over a selection would be read as happening in wrong position.
|
|
20
|
+
|
|
1
21
|
## 6.39.15 (2026-02-20)
|
|
2
22
|
|
|
3
23
|
### Bug fixes
|
package/dist/index.cjs
CHANGED
|
@@ -624,16 +624,16 @@ function scrollRectIntoView(dom, rect, side, x, y, xMargin, yMargin, ltr) {
|
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
626
|
}
|
|
627
|
-
function scrollableParents(dom) {
|
|
628
|
-
let doc = dom.ownerDocument, x, y;
|
|
627
|
+
function scrollableParents(dom, getX = true) {
|
|
628
|
+
let doc = dom.ownerDocument, x = null, y = null;
|
|
629
629
|
for (let cur = dom.parentNode; cur;) {
|
|
630
|
-
if (cur == doc.body || (x && y)) {
|
|
630
|
+
if (cur == doc.body || ((!getX || x) && y)) {
|
|
631
631
|
break;
|
|
632
632
|
}
|
|
633
633
|
else if (cur.nodeType == 1) {
|
|
634
634
|
if (!y && cur.scrollHeight > cur.clientHeight)
|
|
635
635
|
y = cur;
|
|
636
|
-
if (!x && cur.scrollWidth > cur.clientWidth)
|
|
636
|
+
if (getX && !x && cur.scrollWidth > cur.clientWidth)
|
|
637
637
|
x = cur;
|
|
638
638
|
cur = cur.assignedSlot || cur.parentNode;
|
|
639
639
|
}
|
|
@@ -758,6 +758,8 @@ function atElementStart(doc, selection) {
|
|
|
758
758
|
}
|
|
759
759
|
}
|
|
760
760
|
function isScrolledToBottom(elt) {
|
|
761
|
+
if (elt instanceof Window)
|
|
762
|
+
return elt.pageYOffset > Math.max(0, elt.document.documentElement.scrollHeight - elt.innerHeight - 4);
|
|
761
763
|
return elt.scrollTop > Math.max(1, elt.scrollHeight - elt.clientHeight - 4);
|
|
762
764
|
}
|
|
763
765
|
function textNodeBefore(startNode, startOffset) {
|
|
@@ -2656,7 +2658,7 @@ class TileUpdate {
|
|
|
2656
2658
|
}
|
|
2657
2659
|
else if (tile.isText()) {
|
|
2658
2660
|
this.builder.ensureLine(null);
|
|
2659
|
-
if (!from && to == tile.length) {
|
|
2661
|
+
if (!from && to == tile.length && !this.cache.reused.has(tile)) {
|
|
2660
2662
|
this.builder.addText(tile.text, activeMarks, openMarks, this.cache.reuse(tile));
|
|
2661
2663
|
}
|
|
2662
2664
|
else {
|
|
@@ -2960,6 +2962,8 @@ class DocView {
|
|
|
2960
2962
|
if (composition || changes.length) {
|
|
2961
2963
|
let oldTile = this.tile;
|
|
2962
2964
|
let builder = new TileUpdate(this.view, oldTile, this.blockWrappers, this.decorations, this.dynamicDecorationMap);
|
|
2965
|
+
if (composition && Tile.get(composition.text))
|
|
2966
|
+
builder.cache.reused.set(Tile.get(composition.text), 2 /* Reused.DOM */);
|
|
2963
2967
|
this.tile = builder.run(changes, composition);
|
|
2964
2968
|
destroyDropped(oldTile, builder.cache.reused);
|
|
2965
2969
|
}
|
|
@@ -3859,6 +3863,9 @@ class InlineCoordsScan {
|
|
|
3859
3863
|
if (rects)
|
|
3860
3864
|
for (let i = 0; i < rects.length; i++) {
|
|
3861
3865
|
let rect = rects[i], side = 0;
|
|
3866
|
+
// Ignore empty rectangles when there are other rectangles
|
|
3867
|
+
if (rect.width == 0 && rects.length > 1)
|
|
3868
|
+
continue;
|
|
3862
3869
|
if (rect.bottom < this.y) {
|
|
3863
3870
|
if (!above || above.bottom < rect.bottom)
|
|
3864
3871
|
above = rect;
|
|
@@ -4082,7 +4089,7 @@ class DOMChange {
|
|
|
4082
4089
|
this.bounds = null;
|
|
4083
4090
|
this.text = "";
|
|
4084
4091
|
this.domChanged = start > -1;
|
|
4085
|
-
let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
|
|
4092
|
+
let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView, curSel = view.state.selection;
|
|
4086
4093
|
if (view.state.readOnly && start > -1) {
|
|
4087
4094
|
// Ignore changes when the editor is read-only
|
|
4088
4095
|
this.newSel = null;
|
|
@@ -4098,18 +4105,18 @@ class DOMChange {
|
|
|
4098
4105
|
let domSel = view.observer.selectionRange;
|
|
4099
4106
|
let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ||
|
|
4100
4107
|
!contains(view.contentDOM, domSel.focusNode)
|
|
4101
|
-
?
|
|
4108
|
+
? curSel.main.head
|
|
4102
4109
|
: view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
|
|
4103
4110
|
let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset ||
|
|
4104
4111
|
!contains(view.contentDOM, domSel.anchorNode)
|
|
4105
|
-
?
|
|
4112
|
+
? curSel.main.anchor
|
|
4106
4113
|
: view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
|
|
4107
4114
|
// iOS will refuse to select the block gaps when doing
|
|
4108
4115
|
// select-all.
|
|
4109
4116
|
// Chrome will put the selection *inside* them, confusing
|
|
4110
4117
|
// posFromDOM
|
|
4111
4118
|
let vp = view.viewport;
|
|
4112
|
-
if ((browser.ios || browser.chrome) &&
|
|
4119
|
+
if ((browser.ios || browser.chrome) && curSel.main.empty && head != anchor &&
|
|
4113
4120
|
(vp.from > 0 || vp.to < view.state.doc.length)) {
|
|
4114
4121
|
let from = Math.min(head, anchor), to = Math.max(head, anchor);
|
|
4115
4122
|
let offFrom = vp.from - from, offTo = vp.to - to;
|
|
@@ -4118,10 +4125,22 @@ class DOMChange {
|
|
|
4118
4125
|
anchor = view.state.doc.length;
|
|
4119
4126
|
}
|
|
4120
4127
|
}
|
|
4121
|
-
if (view.inputState.composing > -1 &&
|
|
4122
|
-
this.newSel =
|
|
4123
|
-
|
|
4128
|
+
if (view.inputState.composing > -1 && curSel.ranges.length > 1) {
|
|
4129
|
+
this.newSel = curSel.replaceRange(state.EditorSelection.range(anchor, head));
|
|
4130
|
+
}
|
|
4131
|
+
else if (view.lineWrapping && anchor == head && !(curSel.main.empty && curSel.main.head == head) &&
|
|
4132
|
+
view.inputState.lastTouchTime > Date.now() - 100) {
|
|
4133
|
+
// If this is a cursor selection change in a line-wrapping
|
|
4134
|
+
// editor that may have been a touch, use the last touch
|
|
4135
|
+
// position to assign a side to the cursor.
|
|
4136
|
+
let before = view.coordsAtPos(head, -1), assoc = 0;
|
|
4137
|
+
if (before)
|
|
4138
|
+
assoc = view.inputState.lastTouchY <= before.bottom ? -1 : 1;
|
|
4139
|
+
this.newSel = state.EditorSelection.create([state.EditorSelection.cursor(head, assoc)]);
|
|
4140
|
+
}
|
|
4141
|
+
else {
|
|
4124
4142
|
this.newSel = state.EditorSelection.single(anchor, head);
|
|
4143
|
+
}
|
|
4125
4144
|
}
|
|
4126
4145
|
}
|
|
4127
4146
|
}
|
|
@@ -4157,7 +4176,7 @@ function domBoundsAround(tile, from, to, offset) {
|
|
|
4157
4176
|
}
|
|
4158
4177
|
function applyDOMChange(view, domChange) {
|
|
4159
4178
|
let change;
|
|
4160
|
-
let { newSel } = domChange, sel =
|
|
4179
|
+
let { newSel } = domChange, { state: state$1 } = view, sel = state$1.selection.main;
|
|
4161
4180
|
let lastKey = view.inputState.lastKeyTime > Date.now() - 100 ? view.inputState.lastKeyCode : -1;
|
|
4162
4181
|
if (domChange.bounds) {
|
|
4163
4182
|
let { from, to } = domChange.bounds;
|
|
@@ -4168,8 +4187,15 @@ function applyDOMChange(view, domChange) {
|
|
|
4168
4187
|
preferredPos = sel.to;
|
|
4169
4188
|
preferredSide = "end";
|
|
4170
4189
|
}
|
|
4171
|
-
let
|
|
4172
|
-
if (
|
|
4190
|
+
let cmp = state$1.doc.sliceString(from, to, LineBreakPlaceholder), selEnd, diff;
|
|
4191
|
+
if (!sel.empty && sel.from >= from && sel.to <= to && (domChange.typeOver || cmp != domChange.text) &&
|
|
4192
|
+
cmp.slice(0, sel.from - from) == domChange.text.slice(0, sel.from - from) &&
|
|
4193
|
+
cmp.slice(sel.to - from) == domChange.text.slice(selEnd = domChange.text.length - (cmp.length - (sel.to - from)))) {
|
|
4194
|
+
// This looks like a selection replacement
|
|
4195
|
+
change = { from: sel.from, to: sel.to,
|
|
4196
|
+
insert: state.Text.of(domChange.text.slice(sel.from - from, selEnd).split(LineBreakPlaceholder)) };
|
|
4197
|
+
}
|
|
4198
|
+
else if (diff = findDiff(cmp, domChange.text, preferredPos - from, preferredSide)) {
|
|
4173
4199
|
// Chrome inserts two newlines when pressing shift-enter at the
|
|
4174
4200
|
// end of a line. DomChange drops one of those.
|
|
4175
4201
|
if (browser.chrome && lastKey == 13 &&
|
|
@@ -4179,16 +4205,12 @@ function applyDOMChange(view, domChange) {
|
|
|
4179
4205
|
insert: state.Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
|
|
4180
4206
|
}
|
|
4181
4207
|
}
|
|
4182
|
-
else if (newSel && (!view.hasFocus &&
|
|
4208
|
+
else if (newSel && (!view.hasFocus && state$1.facet(editable) || sameSelPos(newSel, sel))) {
|
|
4183
4209
|
newSel = null;
|
|
4184
4210
|
}
|
|
4185
4211
|
if (!change && !newSel)
|
|
4186
4212
|
return false;
|
|
4187
|
-
if (
|
|
4188
|
-
// Heuristic to notice typing over a selected character
|
|
4189
|
-
change = { from: sel.from, to: sel.to, insert: view.state.doc.slice(sel.from, sel.to) };
|
|
4190
|
-
}
|
|
4191
|
-
else if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
|
|
4213
|
+
if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
|
|
4192
4214
|
/^\. ?$/.test(change.insert.toString()) && view.contentDOM.getAttribute("autocorrect") == "off") {
|
|
4193
4215
|
// Detect insert-period-on-double-space Mac and Android behavior,
|
|
4194
4216
|
// and transform it into a regular space insert.
|
|
@@ -4196,18 +4218,7 @@ function applyDOMChange(view, domChange) {
|
|
|
4196
4218
|
newSel = state.EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
|
|
4197
4219
|
change = { from: change.from, to: change.to, insert: state.Text.of([change.insert.toString().replace(".", " ")]) };
|
|
4198
4220
|
}
|
|
4199
|
-
else if (
|
|
4200
|
-
(change.from != sel.from || change.to != sel.to) &&
|
|
4201
|
-
(sel.to - sel.from) - (change.to - change.from) <= 4) {
|
|
4202
|
-
// If the change is inside the selection and covers most of it,
|
|
4203
|
-
// assume it is a selection replace (with identical characters at
|
|
4204
|
-
// the start/end not included in the diff)
|
|
4205
|
-
change = {
|
|
4206
|
-
from: sel.from, to: sel.to,
|
|
4207
|
-
insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
|
|
4208
|
-
};
|
|
4209
|
-
}
|
|
4210
|
-
else if (view.state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
|
|
4221
|
+
else if (state$1.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
|
|
4211
4222
|
view.inputState.insertingTextAt > Date.now() - 50) {
|
|
4212
4223
|
// For a cross-line insertion, Chrome and Safari will crudely take
|
|
4213
4224
|
// the text of the line after the selection, flattening any
|
|
@@ -4216,7 +4227,7 @@ function applyDOMChange(view, domChange) {
|
|
|
4216
4227
|
// replace of the text provided by the beforeinput event.
|
|
4217
4228
|
change = {
|
|
4218
4229
|
from: sel.from, to: sel.to,
|
|
4219
|
-
insert:
|
|
4230
|
+
insert: state$1.toText(view.inputState.insertingText)
|
|
4220
4231
|
};
|
|
4221
4232
|
}
|
|
4222
4233
|
else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
|
|
@@ -4238,7 +4249,7 @@ function applyDOMChange(view, domChange) {
|
|
|
4238
4249
|
scrollIntoView = true;
|
|
4239
4250
|
userEvent = view.inputState.lastSelectionOrigin;
|
|
4240
4251
|
if (userEvent == "select.pointer")
|
|
4241
|
-
newSel = skipAtomsForSelection(
|
|
4252
|
+
newSel = skipAtomsForSelection(state$1.facet(atomicRanges).map(f => f(view)), newSel);
|
|
4242
4253
|
}
|
|
4243
4254
|
view.dispatch({ selection: newSel, scrollIntoView, userEvent });
|
|
4244
4255
|
return true;
|
|
@@ -4416,9 +4427,12 @@ class InputState {
|
|
|
4416
4427
|
this.lastKeyCode = 0;
|
|
4417
4428
|
this.lastKeyTime = 0;
|
|
4418
4429
|
this.lastTouchTime = 0;
|
|
4430
|
+
this.lastTouchX = 0;
|
|
4431
|
+
this.lastTouchY = 0;
|
|
4419
4432
|
this.lastFocusTime = 0;
|
|
4420
4433
|
this.lastScrollTop = 0;
|
|
4421
4434
|
this.lastScrollLeft = 0;
|
|
4435
|
+
this.lastWheelEvent = 0;
|
|
4422
4436
|
// On iOS, some keys need to have their default behavior happen
|
|
4423
4437
|
// (after which we retroactively handle them and reset the DOM) to
|
|
4424
4438
|
// avoid messing up the virtual keyboard state.
|
|
@@ -4848,6 +4862,9 @@ observers.scroll = view => {
|
|
|
4848
4862
|
view.inputState.lastScrollTop = view.scrollDOM.scrollTop;
|
|
4849
4863
|
view.inputState.lastScrollLeft = view.scrollDOM.scrollLeft;
|
|
4850
4864
|
};
|
|
4865
|
+
observers.wheel = observers.mousewheel = view => {
|
|
4866
|
+
view.inputState.lastWheelEvent = Date.now();
|
|
4867
|
+
};
|
|
4851
4868
|
handlers.keydown = (view, event) => {
|
|
4852
4869
|
view.inputState.setSelectionOrigin("select");
|
|
4853
4870
|
if (event.keyCode == 27 && view.inputState.tabFocusMode != 0)
|
|
@@ -4855,8 +4872,13 @@ handlers.keydown = (view, event) => {
|
|
|
4855
4872
|
return false;
|
|
4856
4873
|
};
|
|
4857
4874
|
observers.touchstart = (view, e) => {
|
|
4858
|
-
view.inputState
|
|
4859
|
-
|
|
4875
|
+
let iState = view.inputState, touch = e.targetTouches[0];
|
|
4876
|
+
iState.lastTouchTime = Date.now();
|
|
4877
|
+
if (touch) {
|
|
4878
|
+
iState.lastTouchX = touch.clientX;
|
|
4879
|
+
iState.lastTouchY = touch.clientY;
|
|
4880
|
+
}
|
|
4881
|
+
iState.setSelectionOrigin("select.pointer");
|
|
4860
4882
|
};
|
|
4861
4883
|
observers.touchmove = view => {
|
|
4862
4884
|
view.inputState.setSelectionOrigin("select.pointer");
|
|
@@ -6092,7 +6114,8 @@ class LineGapWidget extends WidgetType {
|
|
|
6092
6114
|
get estimatedHeight() { return this.vertical ? this.size : -1; }
|
|
6093
6115
|
}
|
|
6094
6116
|
class ViewState {
|
|
6095
|
-
constructor(state$1) {
|
|
6117
|
+
constructor(view, state$1) {
|
|
6118
|
+
this.view = view;
|
|
6096
6119
|
this.state = state$1;
|
|
6097
6120
|
// These are contentDOM-local coordinates
|
|
6098
6121
|
this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
|
|
@@ -6103,12 +6126,14 @@ class ViewState {
|
|
|
6103
6126
|
this.contentDOMHeight = 0; // contentDOM.getBoundingClientRect().height
|
|
6104
6127
|
this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
|
|
6105
6128
|
this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
|
|
6106
|
-
this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
|
|
6107
|
-
this.scrolledToBottom = false;
|
|
6108
6129
|
// The CSS-transformation scale of the editor (transformed size /
|
|
6109
6130
|
// concrete size)
|
|
6110
6131
|
this.scaleX = 1;
|
|
6111
6132
|
this.scaleY = 1;
|
|
6133
|
+
// Last seen vertical offset of the element at the top of the scroll
|
|
6134
|
+
// container, or top of the window if there's no wrapping scroller
|
|
6135
|
+
this.scrollOffset = 0;
|
|
6136
|
+
this.scrolledToBottom = false;
|
|
6112
6137
|
// The vertical position (document-relative) to which to anchor the
|
|
6113
6138
|
// scroll position. -1 means anchor to the end of the document.
|
|
6114
6139
|
this.scrollAnchorPos = 0;
|
|
@@ -6146,6 +6171,7 @@ class ViewState {
|
|
|
6146
6171
|
this.updateViewportLines();
|
|
6147
6172
|
this.lineGaps = this.ensureLineGaps([]);
|
|
6148
6173
|
this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(this, false)));
|
|
6174
|
+
this.scrollParent = view.scrollDOM;
|
|
6149
6175
|
this.computeVisibleRanges();
|
|
6150
6176
|
}
|
|
6151
6177
|
updateForViewport() {
|
|
@@ -6179,7 +6205,7 @@ class ViewState {
|
|
|
6179
6205
|
let contentChanges = update.changedRanges;
|
|
6180
6206
|
let heightChanges = ChangedRange.extendWithRanges(contentChanges, heightRelevantDecoChanges(prevDeco, this.stateDeco, update ? update.changes : state.ChangeSet.empty(this.state.doc.length)));
|
|
6181
6207
|
let prevHeight = this.heightMap.height;
|
|
6182
|
-
let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.
|
|
6208
|
+
let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollOffset);
|
|
6183
6209
|
clearHeightChangeFlag();
|
|
6184
6210
|
this.heightMap = this.heightMap.applyChanges(this.stateDeco, update.startState.doc, this.heightOracle.setDoc(this.state.doc), heightChanges);
|
|
6185
6211
|
if (this.heightMap.height != prevHeight || heightChangeFlag)
|
|
@@ -6211,8 +6237,8 @@ class ViewState {
|
|
|
6211
6237
|
!update.state.facet(nativeSelectionHidden))
|
|
6212
6238
|
this.mustEnforceCursorAssoc = true;
|
|
6213
6239
|
}
|
|
6214
|
-
measure(
|
|
6215
|
-
let dom = view.contentDOM, style = window.getComputedStyle(dom);
|
|
6240
|
+
measure() {
|
|
6241
|
+
let { view } = this, dom = view.contentDOM, style = window.getComputedStyle(dom);
|
|
6216
6242
|
let oracle = this.heightOracle;
|
|
6217
6243
|
let whiteSpace = style.whiteSpace;
|
|
6218
6244
|
this.defaultTextDirection = style.direction == "rtl" ? exports.Direction.RTL : exports.Direction.LTR;
|
|
@@ -6246,12 +6272,18 @@ class ViewState {
|
|
|
6246
6272
|
this.editorWidth = view.scrollDOM.clientWidth;
|
|
6247
6273
|
result |= 16 /* UpdateFlag.Geometry */;
|
|
6248
6274
|
}
|
|
6249
|
-
let
|
|
6250
|
-
if (
|
|
6275
|
+
let scrollParent = scrollableParents(this.view.contentDOM, false).y;
|
|
6276
|
+
if (scrollParent != this.scrollParent) {
|
|
6277
|
+
this.scrollParent = scrollParent;
|
|
6278
|
+
this.scrollAnchorHeight = -1;
|
|
6279
|
+
this.scrollOffset = 0;
|
|
6280
|
+
}
|
|
6281
|
+
let scrollOffset = this.getScrollOffset();
|
|
6282
|
+
if (this.scrollOffset != scrollOffset) {
|
|
6251
6283
|
this.scrollAnchorHeight = -1;
|
|
6252
|
-
this.
|
|
6284
|
+
this.scrollOffset = scrollOffset;
|
|
6253
6285
|
}
|
|
6254
|
-
this.scrolledToBottom = isScrolledToBottom(view.
|
|
6286
|
+
this.scrolledToBottom = isScrolledToBottom(this.scrollParent || view.win);
|
|
6255
6287
|
// Pixel viewport
|
|
6256
6288
|
let pixelViewport = (this.printing ? fullPixelRange : visiblePixelRange)(dom, this.paddingTop);
|
|
6257
6289
|
let dTop = pixelViewport.top - this.pixelViewport.top, dBottom = pixelViewport.bottom - this.pixelViewport.bottom;
|
|
@@ -6528,9 +6560,14 @@ class ViewState {
|
|
|
6528
6560
|
this.viewportLines.find(l => l.top <= height && l.bottom >= height)) ||
|
|
6529
6561
|
scaleBlock(this.heightMap.lineAt(this.scaler.fromDOM(height), QueryType.ByHeight, this.heightOracle, 0, 0), this.scaler);
|
|
6530
6562
|
}
|
|
6531
|
-
|
|
6532
|
-
let
|
|
6533
|
-
|
|
6563
|
+
getScrollOffset() {
|
|
6564
|
+
let base = this.scrollParent == this.view.scrollDOM ? this.scrollParent.scrollTop
|
|
6565
|
+
: (this.scrollParent ? this.scrollParent.getBoundingClientRect().top : 0) - this.view.contentDOM.getBoundingClientRect().top;
|
|
6566
|
+
return base * this.scaleY;
|
|
6567
|
+
}
|
|
6568
|
+
scrollAnchorAt(scrollOffset) {
|
|
6569
|
+
let block = this.lineBlockAtHeight(scrollOffset + 8);
|
|
6570
|
+
return block.from >= this.viewport.from || this.viewportLines[0].top - scrollOffset > 200 ? block : this.viewportLines[0];
|
|
6534
6571
|
}
|
|
6535
6572
|
elementAtHeight(height) {
|
|
6536
6573
|
return scaleBlock(this.heightMap.blockAt(this.scaler.fromDOM(height), this.heightOracle, 0, 0), this.scaler);
|
|
@@ -6783,6 +6820,21 @@ const baseTheme$1 = buildTheme("." + baseThemeID, {
|
|
|
6783
6820
|
"&dark .cm-cursor": {
|
|
6784
6821
|
borderLeftColor: "#ddd"
|
|
6785
6822
|
},
|
|
6823
|
+
".cm-selectionHandle": {
|
|
6824
|
+
backgroundColor: "currentColor",
|
|
6825
|
+
width: "1.5px"
|
|
6826
|
+
},
|
|
6827
|
+
".cm-selectionHandle-start::before, .cm-selectionHandle-end::before": {
|
|
6828
|
+
content: '""',
|
|
6829
|
+
backgroundColor: "inherit",
|
|
6830
|
+
borderRadius: "50%",
|
|
6831
|
+
width: "8px",
|
|
6832
|
+
height: "8px",
|
|
6833
|
+
position: "absolute",
|
|
6834
|
+
left: "-3.25px"
|
|
6835
|
+
},
|
|
6836
|
+
".cm-selectionHandle-start::before": { top: "-8px" },
|
|
6837
|
+
".cm-selectionHandle-end::before": { bottom: "-8px" },
|
|
6786
6838
|
".cm-dropCursor": {
|
|
6787
6839
|
position: "absolute"
|
|
6788
6840
|
},
|
|
@@ -7777,7 +7829,7 @@ class EditorView {
|
|
|
7777
7829
|
((trs) => this.update(trs));
|
|
7778
7830
|
this.dispatch = this.dispatch.bind(this);
|
|
7779
7831
|
this._root = (config.root || getRoot(config.parent) || document);
|
|
7780
|
-
this.viewState = new ViewState(config.state || state.EditorState.create(config));
|
|
7832
|
+
this.viewState = new ViewState(this, config.state || state.EditorState.create(config));
|
|
7781
7833
|
if (config.scrollTo && config.scrollTo.is(scrollIntoView))
|
|
7782
7834
|
this.viewState.scrollTarget = config.scrollTo.value.clip(this.viewState.state);
|
|
7783
7835
|
this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec));
|
|
@@ -7932,7 +7984,7 @@ class EditorView {
|
|
|
7932
7984
|
try {
|
|
7933
7985
|
for (let plugin of this.plugins)
|
|
7934
7986
|
plugin.destroy(this);
|
|
7935
|
-
this.viewState = new ViewState(newState);
|
|
7987
|
+
this.viewState = new ViewState(this, newState);
|
|
7936
7988
|
this.plugins = newState.facet(viewPlugin).map(spec => new PluginInstance(spec));
|
|
7937
7989
|
this.pluginMap.clear();
|
|
7938
7990
|
for (let plugin of this.plugins)
|
|
@@ -8011,26 +8063,26 @@ class EditorView {
|
|
|
8011
8063
|
if (flush)
|
|
8012
8064
|
this.observer.forceFlush();
|
|
8013
8065
|
let updated = null;
|
|
8014
|
-
let
|
|
8066
|
+
let scroll = this.viewState.scrollParent, scrollOffset = this.viewState.getScrollOffset();
|
|
8015
8067
|
let { scrollAnchorPos, scrollAnchorHeight } = this.viewState;
|
|
8016
|
-
if (Math.abs(
|
|
8068
|
+
if (Math.abs(scrollOffset - this.viewState.scrollOffset) > 1)
|
|
8017
8069
|
scrollAnchorHeight = -1;
|
|
8018
8070
|
this.viewState.scrollAnchorHeight = -1;
|
|
8019
8071
|
try {
|
|
8020
8072
|
for (let i = 0;; i++) {
|
|
8021
8073
|
if (scrollAnchorHeight < 0) {
|
|
8022
|
-
if (isScrolledToBottom(
|
|
8074
|
+
if (isScrolledToBottom(scroll || this.win)) {
|
|
8023
8075
|
scrollAnchorPos = -1;
|
|
8024
8076
|
scrollAnchorHeight = this.viewState.heightMap.height;
|
|
8025
8077
|
}
|
|
8026
8078
|
else {
|
|
8027
|
-
let block = this.viewState.scrollAnchorAt(
|
|
8079
|
+
let block = this.viewState.scrollAnchorAt(scrollOffset);
|
|
8028
8080
|
scrollAnchorPos = block.from;
|
|
8029
8081
|
scrollAnchorHeight = block.top;
|
|
8030
8082
|
}
|
|
8031
8083
|
}
|
|
8032
8084
|
this.updateState = 1 /* UpdateState.Measuring */;
|
|
8033
|
-
let changed = this.viewState.measure(
|
|
8085
|
+
let changed = this.viewState.measure();
|
|
8034
8086
|
if (!changed && !this.measureRequests.length && this.viewState.scrollTarget == null)
|
|
8035
8087
|
break;
|
|
8036
8088
|
if (i > 5) {
|
|
@@ -8091,10 +8143,15 @@ class EditorView {
|
|
|
8091
8143
|
else {
|
|
8092
8144
|
let newAnchorHeight = scrollAnchorPos < 0 ? this.viewState.heightMap.height :
|
|
8093
8145
|
this.viewState.lineBlockAt(scrollAnchorPos).top;
|
|
8094
|
-
let diff = newAnchorHeight - scrollAnchorHeight;
|
|
8095
|
-
if (diff > 1 || diff < -1)
|
|
8096
|
-
|
|
8097
|
-
|
|
8146
|
+
let diff = (newAnchorHeight - scrollAnchorHeight) / this.scaleY;
|
|
8147
|
+
if ((diff > 1 || diff < -1) &&
|
|
8148
|
+
(scroll == this.scrollDOM || this.hasFocus ||
|
|
8149
|
+
Math.max(this.inputState.lastWheelEvent, this.inputState.lastTouchTime) > Date.now() - 100)) {
|
|
8150
|
+
scrollOffset = scrollOffset + diff;
|
|
8151
|
+
if (scroll)
|
|
8152
|
+
scroll.scrollTop += diff;
|
|
8153
|
+
else
|
|
8154
|
+
this.win.scrollBy(0, diff);
|
|
8098
8155
|
scrollAnchorHeight = -1;
|
|
8099
8156
|
continue;
|
|
8100
8157
|
}
|
|
@@ -9342,7 +9399,8 @@ const selectionConfig = state.Facet.define({
|
|
|
9342
9399
|
combine(configs) {
|
|
9343
9400
|
return state.combineConfig(configs, {
|
|
9344
9401
|
cursorBlinkRate: 1200,
|
|
9345
|
-
drawRangeCursor: true
|
|
9402
|
+
drawRangeCursor: true,
|
|
9403
|
+
iosSelectionHandles: true
|
|
9346
9404
|
}, {
|
|
9347
9405
|
cursorBlinkRate: (a, b) => Math.min(a, b),
|
|
9348
9406
|
drawRangeCursor: (a, b) => a || b
|
|
@@ -9394,7 +9452,7 @@ const cursorLayer = layer({
|
|
|
9394
9452
|
let cursors = [];
|
|
9395
9453
|
for (let r of state$1.selection.ranges) {
|
|
9396
9454
|
let prim = r == state$1.selection.main;
|
|
9397
|
-
if (r.empty || conf.drawRangeCursor) {
|
|
9455
|
+
if (r.empty || conf.drawRangeCursor && !(prim && browser.ios && conf.iosSelectionHandles)) {
|
|
9398
9456
|
let className = prim ? "cm-cursor cm-cursor-primary" : "cm-cursor cm-cursor-secondary";
|
|
9399
9457
|
let cursor = r.empty ? r : state.EditorSelection.cursor(r.head, r.head > r.anchor ? -1 : 1);
|
|
9400
9458
|
for (let piece of RectangleMarker.forRange(view, className, cursor))
|
|
@@ -9422,8 +9480,19 @@ function setBlinkRate(state, dom) {
|
|
|
9422
9480
|
const selectionLayer = layer({
|
|
9423
9481
|
above: false,
|
|
9424
9482
|
markers(view) {
|
|
9425
|
-
|
|
9426
|
-
|
|
9483
|
+
let markers = [], { main, ranges } = view.state.selection;
|
|
9484
|
+
for (let r of ranges)
|
|
9485
|
+
if (!r.empty) {
|
|
9486
|
+
for (let marker of RectangleMarker.forRange(view, "cm-selectionBackground", r))
|
|
9487
|
+
markers.push(marker);
|
|
9488
|
+
}
|
|
9489
|
+
if (browser.ios && !main.empty && view.state.facet(selectionConfig).iosSelectionHandles) {
|
|
9490
|
+
for (let piece of RectangleMarker.forRange(view, "cm-selectionHandle cm-selectionHandle-start", state.EditorSelection.cursor(main.from, 1)))
|
|
9491
|
+
markers.push(piece);
|
|
9492
|
+
for (let piece of RectangleMarker.forRange(view, "cm-selectionHandle cm-selectionHandle-end", state.EditorSelection.cursor(main.to, 1)))
|
|
9493
|
+
markers.push(piece);
|
|
9494
|
+
}
|
|
9495
|
+
return markers;
|
|
9427
9496
|
},
|
|
9428
9497
|
update(update, dom) {
|
|
9429
9498
|
return update.docChanged || update.selectionSet || update.viewportChanged || configChanged(update);
|
package/dist/index.d.cts
CHANGED
|
@@ -1573,6 +1573,12 @@ type SelectionConfig = {
|
|
|
1573
1573
|
true.
|
|
1574
1574
|
*/
|
|
1575
1575
|
drawRangeCursor?: boolean;
|
|
1576
|
+
/**
|
|
1577
|
+
Because hiding the cursor also hides the selection handles in
|
|
1578
|
+
the iOS browser, when this is enabled (the default), the
|
|
1579
|
+
extension draws handles on the side of the selection in iOS.
|
|
1580
|
+
*/
|
|
1581
|
+
iosSelectionHandles?: boolean;
|
|
1576
1582
|
};
|
|
1577
1583
|
/**
|
|
1578
1584
|
Returns an extension that hides the browser's native selection and
|
package/dist/index.d.ts
CHANGED
|
@@ -1573,6 +1573,12 @@ type SelectionConfig = {
|
|
|
1573
1573
|
true.
|
|
1574
1574
|
*/
|
|
1575
1575
|
drawRangeCursor?: boolean;
|
|
1576
|
+
/**
|
|
1577
|
+
Because hiding the cursor also hides the selection handles in
|
|
1578
|
+
the iOS browser, when this is enabled (the default), the
|
|
1579
|
+
extension draws handles on the side of the selection in iOS.
|
|
1580
|
+
*/
|
|
1581
|
+
iosSelectionHandles?: boolean;
|
|
1576
1582
|
};
|
|
1577
1583
|
/**
|
|
1578
1584
|
Returns an extension that hides the browser's native selection and
|
package/dist/index.js
CHANGED
|
@@ -621,16 +621,16 @@ function scrollRectIntoView(dom, rect, side, x, y, xMargin, yMargin, ltr) {
|
|
|
621
621
|
}
|
|
622
622
|
}
|
|
623
623
|
}
|
|
624
|
-
function scrollableParents(dom) {
|
|
625
|
-
let doc = dom.ownerDocument, x, y;
|
|
624
|
+
function scrollableParents(dom, getX = true) {
|
|
625
|
+
let doc = dom.ownerDocument, x = null, y = null;
|
|
626
626
|
for (let cur = dom.parentNode; cur;) {
|
|
627
|
-
if (cur == doc.body || (x && y)) {
|
|
627
|
+
if (cur == doc.body || ((!getX || x) && y)) {
|
|
628
628
|
break;
|
|
629
629
|
}
|
|
630
630
|
else if (cur.nodeType == 1) {
|
|
631
631
|
if (!y && cur.scrollHeight > cur.clientHeight)
|
|
632
632
|
y = cur;
|
|
633
|
-
if (!x && cur.scrollWidth > cur.clientWidth)
|
|
633
|
+
if (getX && !x && cur.scrollWidth > cur.clientWidth)
|
|
634
634
|
x = cur;
|
|
635
635
|
cur = cur.assignedSlot || cur.parentNode;
|
|
636
636
|
}
|
|
@@ -755,6 +755,8 @@ function atElementStart(doc, selection) {
|
|
|
755
755
|
}
|
|
756
756
|
}
|
|
757
757
|
function isScrolledToBottom(elt) {
|
|
758
|
+
if (elt instanceof Window)
|
|
759
|
+
return elt.pageYOffset > Math.max(0, elt.document.documentElement.scrollHeight - elt.innerHeight - 4);
|
|
758
760
|
return elt.scrollTop > Math.max(1, elt.scrollHeight - elt.clientHeight - 4);
|
|
759
761
|
}
|
|
760
762
|
function textNodeBefore(startNode, startOffset) {
|
|
@@ -2652,7 +2654,7 @@ class TileUpdate {
|
|
|
2652
2654
|
}
|
|
2653
2655
|
else if (tile.isText()) {
|
|
2654
2656
|
this.builder.ensureLine(null);
|
|
2655
|
-
if (!from && to == tile.length) {
|
|
2657
|
+
if (!from && to == tile.length && !this.cache.reused.has(tile)) {
|
|
2656
2658
|
this.builder.addText(tile.text, activeMarks, openMarks, this.cache.reuse(tile));
|
|
2657
2659
|
}
|
|
2658
2660
|
else {
|
|
@@ -2956,6 +2958,8 @@ class DocView {
|
|
|
2956
2958
|
if (composition || changes.length) {
|
|
2957
2959
|
let oldTile = this.tile;
|
|
2958
2960
|
let builder = new TileUpdate(this.view, oldTile, this.blockWrappers, this.decorations, this.dynamicDecorationMap);
|
|
2961
|
+
if (composition && Tile.get(composition.text))
|
|
2962
|
+
builder.cache.reused.set(Tile.get(composition.text), 2 /* Reused.DOM */);
|
|
2959
2963
|
this.tile = builder.run(changes, composition);
|
|
2960
2964
|
destroyDropped(oldTile, builder.cache.reused);
|
|
2961
2965
|
}
|
|
@@ -3855,6 +3859,9 @@ class InlineCoordsScan {
|
|
|
3855
3859
|
if (rects)
|
|
3856
3860
|
for (let i = 0; i < rects.length; i++) {
|
|
3857
3861
|
let rect = rects[i], side = 0;
|
|
3862
|
+
// Ignore empty rectangles when there are other rectangles
|
|
3863
|
+
if (rect.width == 0 && rects.length > 1)
|
|
3864
|
+
continue;
|
|
3858
3865
|
if (rect.bottom < this.y) {
|
|
3859
3866
|
if (!above || above.bottom < rect.bottom)
|
|
3860
3867
|
above = rect;
|
|
@@ -4078,7 +4085,7 @@ class DOMChange {
|
|
|
4078
4085
|
this.bounds = null;
|
|
4079
4086
|
this.text = "";
|
|
4080
4087
|
this.domChanged = start > -1;
|
|
4081
|
-
let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
|
|
4088
|
+
let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView, curSel = view.state.selection;
|
|
4082
4089
|
if (view.state.readOnly && start > -1) {
|
|
4083
4090
|
// Ignore changes when the editor is read-only
|
|
4084
4091
|
this.newSel = null;
|
|
@@ -4094,18 +4101,18 @@ class DOMChange {
|
|
|
4094
4101
|
let domSel = view.observer.selectionRange;
|
|
4095
4102
|
let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ||
|
|
4096
4103
|
!contains(view.contentDOM, domSel.focusNode)
|
|
4097
|
-
?
|
|
4104
|
+
? curSel.main.head
|
|
4098
4105
|
: view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
|
|
4099
4106
|
let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset ||
|
|
4100
4107
|
!contains(view.contentDOM, domSel.anchorNode)
|
|
4101
|
-
?
|
|
4108
|
+
? curSel.main.anchor
|
|
4102
4109
|
: view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
|
|
4103
4110
|
// iOS will refuse to select the block gaps when doing
|
|
4104
4111
|
// select-all.
|
|
4105
4112
|
// Chrome will put the selection *inside* them, confusing
|
|
4106
4113
|
// posFromDOM
|
|
4107
4114
|
let vp = view.viewport;
|
|
4108
|
-
if ((browser.ios || browser.chrome) &&
|
|
4115
|
+
if ((browser.ios || browser.chrome) && curSel.main.empty && head != anchor &&
|
|
4109
4116
|
(vp.from > 0 || vp.to < view.state.doc.length)) {
|
|
4110
4117
|
let from = Math.min(head, anchor), to = Math.max(head, anchor);
|
|
4111
4118
|
let offFrom = vp.from - from, offTo = vp.to - to;
|
|
@@ -4114,10 +4121,22 @@ class DOMChange {
|
|
|
4114
4121
|
anchor = view.state.doc.length;
|
|
4115
4122
|
}
|
|
4116
4123
|
}
|
|
4117
|
-
if (view.inputState.composing > -1 &&
|
|
4118
|
-
this.newSel =
|
|
4119
|
-
|
|
4124
|
+
if (view.inputState.composing > -1 && curSel.ranges.length > 1) {
|
|
4125
|
+
this.newSel = curSel.replaceRange(EditorSelection.range(anchor, head));
|
|
4126
|
+
}
|
|
4127
|
+
else if (view.lineWrapping && anchor == head && !(curSel.main.empty && curSel.main.head == head) &&
|
|
4128
|
+
view.inputState.lastTouchTime > Date.now() - 100) {
|
|
4129
|
+
// If this is a cursor selection change in a line-wrapping
|
|
4130
|
+
// editor that may have been a touch, use the last touch
|
|
4131
|
+
// position to assign a side to the cursor.
|
|
4132
|
+
let before = view.coordsAtPos(head, -1), assoc = 0;
|
|
4133
|
+
if (before)
|
|
4134
|
+
assoc = view.inputState.lastTouchY <= before.bottom ? -1 : 1;
|
|
4135
|
+
this.newSel = EditorSelection.create([EditorSelection.cursor(head, assoc)]);
|
|
4136
|
+
}
|
|
4137
|
+
else {
|
|
4120
4138
|
this.newSel = EditorSelection.single(anchor, head);
|
|
4139
|
+
}
|
|
4121
4140
|
}
|
|
4122
4141
|
}
|
|
4123
4142
|
}
|
|
@@ -4153,7 +4172,7 @@ function domBoundsAround(tile, from, to, offset) {
|
|
|
4153
4172
|
}
|
|
4154
4173
|
function applyDOMChange(view, domChange) {
|
|
4155
4174
|
let change;
|
|
4156
|
-
let { newSel } = domChange, sel =
|
|
4175
|
+
let { newSel } = domChange, { state } = view, sel = state.selection.main;
|
|
4157
4176
|
let lastKey = view.inputState.lastKeyTime > Date.now() - 100 ? view.inputState.lastKeyCode : -1;
|
|
4158
4177
|
if (domChange.bounds) {
|
|
4159
4178
|
let { from, to } = domChange.bounds;
|
|
@@ -4164,8 +4183,15 @@ function applyDOMChange(view, domChange) {
|
|
|
4164
4183
|
preferredPos = sel.to;
|
|
4165
4184
|
preferredSide = "end";
|
|
4166
4185
|
}
|
|
4167
|
-
let
|
|
4168
|
-
if (
|
|
4186
|
+
let cmp = state.doc.sliceString(from, to, LineBreakPlaceholder), selEnd, diff;
|
|
4187
|
+
if (!sel.empty && sel.from >= from && sel.to <= to && (domChange.typeOver || cmp != domChange.text) &&
|
|
4188
|
+
cmp.slice(0, sel.from - from) == domChange.text.slice(0, sel.from - from) &&
|
|
4189
|
+
cmp.slice(sel.to - from) == domChange.text.slice(selEnd = domChange.text.length - (cmp.length - (sel.to - from)))) {
|
|
4190
|
+
// This looks like a selection replacement
|
|
4191
|
+
change = { from: sel.from, to: sel.to,
|
|
4192
|
+
insert: Text.of(domChange.text.slice(sel.from - from, selEnd).split(LineBreakPlaceholder)) };
|
|
4193
|
+
}
|
|
4194
|
+
else if (diff = findDiff(cmp, domChange.text, preferredPos - from, preferredSide)) {
|
|
4169
4195
|
// Chrome inserts two newlines when pressing shift-enter at the
|
|
4170
4196
|
// end of a line. DomChange drops one of those.
|
|
4171
4197
|
if (browser.chrome && lastKey == 13 &&
|
|
@@ -4175,16 +4201,12 @@ function applyDOMChange(view, domChange) {
|
|
|
4175
4201
|
insert: Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
|
|
4176
4202
|
}
|
|
4177
4203
|
}
|
|
4178
|
-
else if (newSel && (!view.hasFocus &&
|
|
4204
|
+
else if (newSel && (!view.hasFocus && state.facet(editable) || sameSelPos(newSel, sel))) {
|
|
4179
4205
|
newSel = null;
|
|
4180
4206
|
}
|
|
4181
4207
|
if (!change && !newSel)
|
|
4182
4208
|
return false;
|
|
4183
|
-
if (
|
|
4184
|
-
// Heuristic to notice typing over a selected character
|
|
4185
|
-
change = { from: sel.from, to: sel.to, insert: view.state.doc.slice(sel.from, sel.to) };
|
|
4186
|
-
}
|
|
4187
|
-
else if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
|
|
4209
|
+
if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
|
|
4188
4210
|
/^\. ?$/.test(change.insert.toString()) && view.contentDOM.getAttribute("autocorrect") == "off") {
|
|
4189
4211
|
// Detect insert-period-on-double-space Mac and Android behavior,
|
|
4190
4212
|
// and transform it into a regular space insert.
|
|
@@ -4192,18 +4214,7 @@ function applyDOMChange(view, domChange) {
|
|
|
4192
4214
|
newSel = EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
|
|
4193
4215
|
change = { from: change.from, to: change.to, insert: Text.of([change.insert.toString().replace(".", " ")]) };
|
|
4194
4216
|
}
|
|
4195
|
-
else if (
|
|
4196
|
-
(change.from != sel.from || change.to != sel.to) &&
|
|
4197
|
-
(sel.to - sel.from) - (change.to - change.from) <= 4) {
|
|
4198
|
-
// If the change is inside the selection and covers most of it,
|
|
4199
|
-
// assume it is a selection replace (with identical characters at
|
|
4200
|
-
// the start/end not included in the diff)
|
|
4201
|
-
change = {
|
|
4202
|
-
from: sel.from, to: sel.to,
|
|
4203
|
-
insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
|
|
4204
|
-
};
|
|
4205
|
-
}
|
|
4206
|
-
else if (view.state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
|
|
4217
|
+
else if (state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
|
|
4207
4218
|
view.inputState.insertingTextAt > Date.now() - 50) {
|
|
4208
4219
|
// For a cross-line insertion, Chrome and Safari will crudely take
|
|
4209
4220
|
// the text of the line after the selection, flattening any
|
|
@@ -4212,7 +4223,7 @@ function applyDOMChange(view, domChange) {
|
|
|
4212
4223
|
// replace of the text provided by the beforeinput event.
|
|
4213
4224
|
change = {
|
|
4214
4225
|
from: sel.from, to: sel.to,
|
|
4215
|
-
insert:
|
|
4226
|
+
insert: state.toText(view.inputState.insertingText)
|
|
4216
4227
|
};
|
|
4217
4228
|
}
|
|
4218
4229
|
else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
|
|
@@ -4234,7 +4245,7 @@ function applyDOMChange(view, domChange) {
|
|
|
4234
4245
|
scrollIntoView = true;
|
|
4235
4246
|
userEvent = view.inputState.lastSelectionOrigin;
|
|
4236
4247
|
if (userEvent == "select.pointer")
|
|
4237
|
-
newSel = skipAtomsForSelection(
|
|
4248
|
+
newSel = skipAtomsForSelection(state.facet(atomicRanges).map(f => f(view)), newSel);
|
|
4238
4249
|
}
|
|
4239
4250
|
view.dispatch({ selection: newSel, scrollIntoView, userEvent });
|
|
4240
4251
|
return true;
|
|
@@ -4412,9 +4423,12 @@ class InputState {
|
|
|
4412
4423
|
this.lastKeyCode = 0;
|
|
4413
4424
|
this.lastKeyTime = 0;
|
|
4414
4425
|
this.lastTouchTime = 0;
|
|
4426
|
+
this.lastTouchX = 0;
|
|
4427
|
+
this.lastTouchY = 0;
|
|
4415
4428
|
this.lastFocusTime = 0;
|
|
4416
4429
|
this.lastScrollTop = 0;
|
|
4417
4430
|
this.lastScrollLeft = 0;
|
|
4431
|
+
this.lastWheelEvent = 0;
|
|
4418
4432
|
// On iOS, some keys need to have their default behavior happen
|
|
4419
4433
|
// (after which we retroactively handle them and reset the DOM) to
|
|
4420
4434
|
// avoid messing up the virtual keyboard state.
|
|
@@ -4844,6 +4858,9 @@ observers.scroll = view => {
|
|
|
4844
4858
|
view.inputState.lastScrollTop = view.scrollDOM.scrollTop;
|
|
4845
4859
|
view.inputState.lastScrollLeft = view.scrollDOM.scrollLeft;
|
|
4846
4860
|
};
|
|
4861
|
+
observers.wheel = observers.mousewheel = view => {
|
|
4862
|
+
view.inputState.lastWheelEvent = Date.now();
|
|
4863
|
+
};
|
|
4847
4864
|
handlers.keydown = (view, event) => {
|
|
4848
4865
|
view.inputState.setSelectionOrigin("select");
|
|
4849
4866
|
if (event.keyCode == 27 && view.inputState.tabFocusMode != 0)
|
|
@@ -4851,8 +4868,13 @@ handlers.keydown = (view, event) => {
|
|
|
4851
4868
|
return false;
|
|
4852
4869
|
};
|
|
4853
4870
|
observers.touchstart = (view, e) => {
|
|
4854
|
-
view.inputState
|
|
4855
|
-
|
|
4871
|
+
let iState = view.inputState, touch = e.targetTouches[0];
|
|
4872
|
+
iState.lastTouchTime = Date.now();
|
|
4873
|
+
if (touch) {
|
|
4874
|
+
iState.lastTouchX = touch.clientX;
|
|
4875
|
+
iState.lastTouchY = touch.clientY;
|
|
4876
|
+
}
|
|
4877
|
+
iState.setSelectionOrigin("select.pointer");
|
|
4856
4878
|
};
|
|
4857
4879
|
observers.touchmove = view => {
|
|
4858
4880
|
view.inputState.setSelectionOrigin("select.pointer");
|
|
@@ -6087,7 +6109,8 @@ class LineGapWidget extends WidgetType {
|
|
|
6087
6109
|
get estimatedHeight() { return this.vertical ? this.size : -1; }
|
|
6088
6110
|
}
|
|
6089
6111
|
class ViewState {
|
|
6090
|
-
constructor(state) {
|
|
6112
|
+
constructor(view, state) {
|
|
6113
|
+
this.view = view;
|
|
6091
6114
|
this.state = state;
|
|
6092
6115
|
// These are contentDOM-local coordinates
|
|
6093
6116
|
this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
|
|
@@ -6098,12 +6121,14 @@ class ViewState {
|
|
|
6098
6121
|
this.contentDOMHeight = 0; // contentDOM.getBoundingClientRect().height
|
|
6099
6122
|
this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
|
|
6100
6123
|
this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
|
|
6101
|
-
this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
|
|
6102
|
-
this.scrolledToBottom = false;
|
|
6103
6124
|
// The CSS-transformation scale of the editor (transformed size /
|
|
6104
6125
|
// concrete size)
|
|
6105
6126
|
this.scaleX = 1;
|
|
6106
6127
|
this.scaleY = 1;
|
|
6128
|
+
// Last seen vertical offset of the element at the top of the scroll
|
|
6129
|
+
// container, or top of the window if there's no wrapping scroller
|
|
6130
|
+
this.scrollOffset = 0;
|
|
6131
|
+
this.scrolledToBottom = false;
|
|
6107
6132
|
// The vertical position (document-relative) to which to anchor the
|
|
6108
6133
|
// scroll position. -1 means anchor to the end of the document.
|
|
6109
6134
|
this.scrollAnchorPos = 0;
|
|
@@ -6141,6 +6166,7 @@ class ViewState {
|
|
|
6141
6166
|
this.updateViewportLines();
|
|
6142
6167
|
this.lineGaps = this.ensureLineGaps([]);
|
|
6143
6168
|
this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(this, false)));
|
|
6169
|
+
this.scrollParent = view.scrollDOM;
|
|
6144
6170
|
this.computeVisibleRanges();
|
|
6145
6171
|
}
|
|
6146
6172
|
updateForViewport() {
|
|
@@ -6174,7 +6200,7 @@ class ViewState {
|
|
|
6174
6200
|
let contentChanges = update.changedRanges;
|
|
6175
6201
|
let heightChanges = ChangedRange.extendWithRanges(contentChanges, heightRelevantDecoChanges(prevDeco, this.stateDeco, update ? update.changes : ChangeSet.empty(this.state.doc.length)));
|
|
6176
6202
|
let prevHeight = this.heightMap.height;
|
|
6177
|
-
let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.
|
|
6203
|
+
let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollOffset);
|
|
6178
6204
|
clearHeightChangeFlag();
|
|
6179
6205
|
this.heightMap = this.heightMap.applyChanges(this.stateDeco, update.startState.doc, this.heightOracle.setDoc(this.state.doc), heightChanges);
|
|
6180
6206
|
if (this.heightMap.height != prevHeight || heightChangeFlag)
|
|
@@ -6206,8 +6232,8 @@ class ViewState {
|
|
|
6206
6232
|
!update.state.facet(nativeSelectionHidden))
|
|
6207
6233
|
this.mustEnforceCursorAssoc = true;
|
|
6208
6234
|
}
|
|
6209
|
-
measure(
|
|
6210
|
-
let dom = view.contentDOM, style = window.getComputedStyle(dom);
|
|
6235
|
+
measure() {
|
|
6236
|
+
let { view } = this, dom = view.contentDOM, style = window.getComputedStyle(dom);
|
|
6211
6237
|
let oracle = this.heightOracle;
|
|
6212
6238
|
let whiteSpace = style.whiteSpace;
|
|
6213
6239
|
this.defaultTextDirection = style.direction == "rtl" ? Direction.RTL : Direction.LTR;
|
|
@@ -6241,12 +6267,18 @@ class ViewState {
|
|
|
6241
6267
|
this.editorWidth = view.scrollDOM.clientWidth;
|
|
6242
6268
|
result |= 16 /* UpdateFlag.Geometry */;
|
|
6243
6269
|
}
|
|
6244
|
-
let
|
|
6245
|
-
if (
|
|
6270
|
+
let scrollParent = scrollableParents(this.view.contentDOM, false).y;
|
|
6271
|
+
if (scrollParent != this.scrollParent) {
|
|
6272
|
+
this.scrollParent = scrollParent;
|
|
6273
|
+
this.scrollAnchorHeight = -1;
|
|
6274
|
+
this.scrollOffset = 0;
|
|
6275
|
+
}
|
|
6276
|
+
let scrollOffset = this.getScrollOffset();
|
|
6277
|
+
if (this.scrollOffset != scrollOffset) {
|
|
6246
6278
|
this.scrollAnchorHeight = -1;
|
|
6247
|
-
this.
|
|
6279
|
+
this.scrollOffset = scrollOffset;
|
|
6248
6280
|
}
|
|
6249
|
-
this.scrolledToBottom = isScrolledToBottom(view.
|
|
6281
|
+
this.scrolledToBottom = isScrolledToBottom(this.scrollParent || view.win);
|
|
6250
6282
|
// Pixel viewport
|
|
6251
6283
|
let pixelViewport = (this.printing ? fullPixelRange : visiblePixelRange)(dom, this.paddingTop);
|
|
6252
6284
|
let dTop = pixelViewport.top - this.pixelViewport.top, dBottom = pixelViewport.bottom - this.pixelViewport.bottom;
|
|
@@ -6523,9 +6555,14 @@ class ViewState {
|
|
|
6523
6555
|
this.viewportLines.find(l => l.top <= height && l.bottom >= height)) ||
|
|
6524
6556
|
scaleBlock(this.heightMap.lineAt(this.scaler.fromDOM(height), QueryType.ByHeight, this.heightOracle, 0, 0), this.scaler);
|
|
6525
6557
|
}
|
|
6526
|
-
|
|
6527
|
-
let
|
|
6528
|
-
|
|
6558
|
+
getScrollOffset() {
|
|
6559
|
+
let base = this.scrollParent == this.view.scrollDOM ? this.scrollParent.scrollTop
|
|
6560
|
+
: (this.scrollParent ? this.scrollParent.getBoundingClientRect().top : 0) - this.view.contentDOM.getBoundingClientRect().top;
|
|
6561
|
+
return base * this.scaleY;
|
|
6562
|
+
}
|
|
6563
|
+
scrollAnchorAt(scrollOffset) {
|
|
6564
|
+
let block = this.lineBlockAtHeight(scrollOffset + 8);
|
|
6565
|
+
return block.from >= this.viewport.from || this.viewportLines[0].top - scrollOffset > 200 ? block : this.viewportLines[0];
|
|
6529
6566
|
}
|
|
6530
6567
|
elementAtHeight(height) {
|
|
6531
6568
|
return scaleBlock(this.heightMap.blockAt(this.scaler.fromDOM(height), this.heightOracle, 0, 0), this.scaler);
|
|
@@ -6778,6 +6815,21 @@ const baseTheme$1 = /*@__PURE__*/buildTheme("." + baseThemeID, {
|
|
|
6778
6815
|
"&dark .cm-cursor": {
|
|
6779
6816
|
borderLeftColor: "#ddd"
|
|
6780
6817
|
},
|
|
6818
|
+
".cm-selectionHandle": {
|
|
6819
|
+
backgroundColor: "currentColor",
|
|
6820
|
+
width: "1.5px"
|
|
6821
|
+
},
|
|
6822
|
+
".cm-selectionHandle-start::before, .cm-selectionHandle-end::before": {
|
|
6823
|
+
content: '""',
|
|
6824
|
+
backgroundColor: "inherit",
|
|
6825
|
+
borderRadius: "50%",
|
|
6826
|
+
width: "8px",
|
|
6827
|
+
height: "8px",
|
|
6828
|
+
position: "absolute",
|
|
6829
|
+
left: "-3.25px"
|
|
6830
|
+
},
|
|
6831
|
+
".cm-selectionHandle-start::before": { top: "-8px" },
|
|
6832
|
+
".cm-selectionHandle-end::before": { bottom: "-8px" },
|
|
6781
6833
|
".cm-dropCursor": {
|
|
6782
6834
|
position: "absolute"
|
|
6783
6835
|
},
|
|
@@ -7772,7 +7824,7 @@ class EditorView {
|
|
|
7772
7824
|
((trs) => this.update(trs));
|
|
7773
7825
|
this.dispatch = this.dispatch.bind(this);
|
|
7774
7826
|
this._root = (config.root || getRoot(config.parent) || document);
|
|
7775
|
-
this.viewState = new ViewState(config.state || EditorState.create(config));
|
|
7827
|
+
this.viewState = new ViewState(this, config.state || EditorState.create(config));
|
|
7776
7828
|
if (config.scrollTo && config.scrollTo.is(scrollIntoView))
|
|
7777
7829
|
this.viewState.scrollTarget = config.scrollTo.value.clip(this.viewState.state);
|
|
7778
7830
|
this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec));
|
|
@@ -7927,7 +7979,7 @@ class EditorView {
|
|
|
7927
7979
|
try {
|
|
7928
7980
|
for (let plugin of this.plugins)
|
|
7929
7981
|
plugin.destroy(this);
|
|
7930
|
-
this.viewState = new ViewState(newState);
|
|
7982
|
+
this.viewState = new ViewState(this, newState);
|
|
7931
7983
|
this.plugins = newState.facet(viewPlugin).map(spec => new PluginInstance(spec));
|
|
7932
7984
|
this.pluginMap.clear();
|
|
7933
7985
|
for (let plugin of this.plugins)
|
|
@@ -8006,26 +8058,26 @@ class EditorView {
|
|
|
8006
8058
|
if (flush)
|
|
8007
8059
|
this.observer.forceFlush();
|
|
8008
8060
|
let updated = null;
|
|
8009
|
-
let
|
|
8061
|
+
let scroll = this.viewState.scrollParent, scrollOffset = this.viewState.getScrollOffset();
|
|
8010
8062
|
let { scrollAnchorPos, scrollAnchorHeight } = this.viewState;
|
|
8011
|
-
if (Math.abs(
|
|
8063
|
+
if (Math.abs(scrollOffset - this.viewState.scrollOffset) > 1)
|
|
8012
8064
|
scrollAnchorHeight = -1;
|
|
8013
8065
|
this.viewState.scrollAnchorHeight = -1;
|
|
8014
8066
|
try {
|
|
8015
8067
|
for (let i = 0;; i++) {
|
|
8016
8068
|
if (scrollAnchorHeight < 0) {
|
|
8017
|
-
if (isScrolledToBottom(
|
|
8069
|
+
if (isScrolledToBottom(scroll || this.win)) {
|
|
8018
8070
|
scrollAnchorPos = -1;
|
|
8019
8071
|
scrollAnchorHeight = this.viewState.heightMap.height;
|
|
8020
8072
|
}
|
|
8021
8073
|
else {
|
|
8022
|
-
let block = this.viewState.scrollAnchorAt(
|
|
8074
|
+
let block = this.viewState.scrollAnchorAt(scrollOffset);
|
|
8023
8075
|
scrollAnchorPos = block.from;
|
|
8024
8076
|
scrollAnchorHeight = block.top;
|
|
8025
8077
|
}
|
|
8026
8078
|
}
|
|
8027
8079
|
this.updateState = 1 /* UpdateState.Measuring */;
|
|
8028
|
-
let changed = this.viewState.measure(
|
|
8080
|
+
let changed = this.viewState.measure();
|
|
8029
8081
|
if (!changed && !this.measureRequests.length && this.viewState.scrollTarget == null)
|
|
8030
8082
|
break;
|
|
8031
8083
|
if (i > 5) {
|
|
@@ -8086,10 +8138,15 @@ class EditorView {
|
|
|
8086
8138
|
else {
|
|
8087
8139
|
let newAnchorHeight = scrollAnchorPos < 0 ? this.viewState.heightMap.height :
|
|
8088
8140
|
this.viewState.lineBlockAt(scrollAnchorPos).top;
|
|
8089
|
-
let diff = newAnchorHeight - scrollAnchorHeight;
|
|
8090
|
-
if (diff > 1 || diff < -1)
|
|
8091
|
-
|
|
8092
|
-
|
|
8141
|
+
let diff = (newAnchorHeight - scrollAnchorHeight) / this.scaleY;
|
|
8142
|
+
if ((diff > 1 || diff < -1) &&
|
|
8143
|
+
(scroll == this.scrollDOM || this.hasFocus ||
|
|
8144
|
+
Math.max(this.inputState.lastWheelEvent, this.inputState.lastTouchTime) > Date.now() - 100)) {
|
|
8145
|
+
scrollOffset = scrollOffset + diff;
|
|
8146
|
+
if (scroll)
|
|
8147
|
+
scroll.scrollTop += diff;
|
|
8148
|
+
else
|
|
8149
|
+
this.win.scrollBy(0, diff);
|
|
8093
8150
|
scrollAnchorHeight = -1;
|
|
8094
8151
|
continue;
|
|
8095
8152
|
}
|
|
@@ -9337,7 +9394,8 @@ const selectionConfig = /*@__PURE__*/Facet.define({
|
|
|
9337
9394
|
combine(configs) {
|
|
9338
9395
|
return combineConfig(configs, {
|
|
9339
9396
|
cursorBlinkRate: 1200,
|
|
9340
|
-
drawRangeCursor: true
|
|
9397
|
+
drawRangeCursor: true,
|
|
9398
|
+
iosSelectionHandles: true
|
|
9341
9399
|
}, {
|
|
9342
9400
|
cursorBlinkRate: (a, b) => Math.min(a, b),
|
|
9343
9401
|
drawRangeCursor: (a, b) => a || b
|
|
@@ -9389,7 +9447,7 @@ const cursorLayer = /*@__PURE__*/layer({
|
|
|
9389
9447
|
let cursors = [];
|
|
9390
9448
|
for (let r of state.selection.ranges) {
|
|
9391
9449
|
let prim = r == state.selection.main;
|
|
9392
|
-
if (r.empty || conf.drawRangeCursor) {
|
|
9450
|
+
if (r.empty || conf.drawRangeCursor && !(prim && browser.ios && conf.iosSelectionHandles)) {
|
|
9393
9451
|
let className = prim ? "cm-cursor cm-cursor-primary" : "cm-cursor cm-cursor-secondary";
|
|
9394
9452
|
let cursor = r.empty ? r : EditorSelection.cursor(r.head, r.head > r.anchor ? -1 : 1);
|
|
9395
9453
|
for (let piece of RectangleMarker.forRange(view, className, cursor))
|
|
@@ -9417,8 +9475,19 @@ function setBlinkRate(state, dom) {
|
|
|
9417
9475
|
const selectionLayer = /*@__PURE__*/layer({
|
|
9418
9476
|
above: false,
|
|
9419
9477
|
markers(view) {
|
|
9420
|
-
|
|
9421
|
-
|
|
9478
|
+
let markers = [], { main, ranges } = view.state.selection;
|
|
9479
|
+
for (let r of ranges)
|
|
9480
|
+
if (!r.empty) {
|
|
9481
|
+
for (let marker of RectangleMarker.forRange(view, "cm-selectionBackground", r))
|
|
9482
|
+
markers.push(marker);
|
|
9483
|
+
}
|
|
9484
|
+
if (browser.ios && !main.empty && view.state.facet(selectionConfig).iosSelectionHandles) {
|
|
9485
|
+
for (let piece of RectangleMarker.forRange(view, "cm-selectionHandle cm-selectionHandle-start", EditorSelection.cursor(main.from, 1)))
|
|
9486
|
+
markers.push(piece);
|
|
9487
|
+
for (let piece of RectangleMarker.forRange(view, "cm-selectionHandle cm-selectionHandle-end", EditorSelection.cursor(main.to, 1)))
|
|
9488
|
+
markers.push(piece);
|
|
9489
|
+
}
|
|
9490
|
+
return markers;
|
|
9422
9491
|
},
|
|
9423
9492
|
update(update, dom) {
|
|
9424
9493
|
return update.docChanged || update.selectionSet || update.viewportChanged || configChanged(update);
|