@codemirror/view 6.39.15 → 6.39.16
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 +10 -0
- package/dist/index.cjs +72 -52
- package/dist/index.js +72 -52
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 6.39.16 (2026-03-02)
|
|
2
|
+
|
|
3
|
+
### Bug fixes
|
|
4
|
+
|
|
5
|
+
Perform scroll stabilization on the document or wrapping scrollable elements, when the user scrolls the editor.
|
|
6
|
+
|
|
7
|
+
Fix an issue where changing decorations right before a composition could end up corrupting the visible DOM.
|
|
8
|
+
|
|
9
|
+
Fix an issue where some types of text input over a selection would be read as happening in wrong position.
|
|
10
|
+
|
|
1
11
|
## 6.39.15 (2026-02-20)
|
|
2
12
|
|
|
3
13
|
### 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
|
}
|
|
@@ -4157,7 +4161,7 @@ function domBoundsAround(tile, from, to, offset) {
|
|
|
4157
4161
|
}
|
|
4158
4162
|
function applyDOMChange(view, domChange) {
|
|
4159
4163
|
let change;
|
|
4160
|
-
let { newSel } = domChange, sel =
|
|
4164
|
+
let { newSel } = domChange, { state: state$1 } = view, sel = state$1.selection.main;
|
|
4161
4165
|
let lastKey = view.inputState.lastKeyTime > Date.now() - 100 ? view.inputState.lastKeyCode : -1;
|
|
4162
4166
|
if (domChange.bounds) {
|
|
4163
4167
|
let { from, to } = domChange.bounds;
|
|
@@ -4168,8 +4172,15 @@ function applyDOMChange(view, domChange) {
|
|
|
4168
4172
|
preferredPos = sel.to;
|
|
4169
4173
|
preferredSide = "end";
|
|
4170
4174
|
}
|
|
4171
|
-
let
|
|
4172
|
-
if (
|
|
4175
|
+
let cmp = state$1.doc.sliceString(from, to, LineBreakPlaceholder), selEnd, diff;
|
|
4176
|
+
if (!sel.empty && sel.from >= from && sel.to <= to && (domChange.typeOver || cmp != domChange.text) &&
|
|
4177
|
+
cmp.slice(0, sel.from - from) == domChange.text.slice(0, sel.from - from) &&
|
|
4178
|
+
cmp.slice(sel.to - from) == domChange.text.slice(selEnd = domChange.text.length - (cmp.length - (sel.to - from)))) {
|
|
4179
|
+
// This looks like a selection replacement
|
|
4180
|
+
change = { from: sel.from, to: sel.to,
|
|
4181
|
+
insert: state.Text.of(domChange.text.slice(sel.from - from, selEnd).split(LineBreakPlaceholder)) };
|
|
4182
|
+
}
|
|
4183
|
+
else if (diff = findDiff(cmp, domChange.text, preferredPos - from, preferredSide)) {
|
|
4173
4184
|
// Chrome inserts two newlines when pressing shift-enter at the
|
|
4174
4185
|
// end of a line. DomChange drops one of those.
|
|
4175
4186
|
if (browser.chrome && lastKey == 13 &&
|
|
@@ -4179,16 +4190,12 @@ function applyDOMChange(view, domChange) {
|
|
|
4179
4190
|
insert: state.Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
|
|
4180
4191
|
}
|
|
4181
4192
|
}
|
|
4182
|
-
else if (newSel && (!view.hasFocus &&
|
|
4193
|
+
else if (newSel && (!view.hasFocus && state$1.facet(editable) || sameSelPos(newSel, sel))) {
|
|
4183
4194
|
newSel = null;
|
|
4184
4195
|
}
|
|
4185
4196
|
if (!change && !newSel)
|
|
4186
4197
|
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 &&
|
|
4198
|
+
if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
|
|
4192
4199
|
/^\. ?$/.test(change.insert.toString()) && view.contentDOM.getAttribute("autocorrect") == "off") {
|
|
4193
4200
|
// Detect insert-period-on-double-space Mac and Android behavior,
|
|
4194
4201
|
// and transform it into a regular space insert.
|
|
@@ -4196,18 +4203,7 @@ function applyDOMChange(view, domChange) {
|
|
|
4196
4203
|
newSel = state.EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
|
|
4197
4204
|
change = { from: change.from, to: change.to, insert: state.Text.of([change.insert.toString().replace(".", " ")]) };
|
|
4198
4205
|
}
|
|
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) &&
|
|
4206
|
+
else if (state$1.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
|
|
4211
4207
|
view.inputState.insertingTextAt > Date.now() - 50) {
|
|
4212
4208
|
// For a cross-line insertion, Chrome and Safari will crudely take
|
|
4213
4209
|
// the text of the line after the selection, flattening any
|
|
@@ -4216,7 +4212,7 @@ function applyDOMChange(view, domChange) {
|
|
|
4216
4212
|
// replace of the text provided by the beforeinput event.
|
|
4217
4213
|
change = {
|
|
4218
4214
|
from: sel.from, to: sel.to,
|
|
4219
|
-
insert:
|
|
4215
|
+
insert: state$1.toText(view.inputState.insertingText)
|
|
4220
4216
|
};
|
|
4221
4217
|
}
|
|
4222
4218
|
else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
|
|
@@ -4238,7 +4234,7 @@ function applyDOMChange(view, domChange) {
|
|
|
4238
4234
|
scrollIntoView = true;
|
|
4239
4235
|
userEvent = view.inputState.lastSelectionOrigin;
|
|
4240
4236
|
if (userEvent == "select.pointer")
|
|
4241
|
-
newSel = skipAtomsForSelection(
|
|
4237
|
+
newSel = skipAtomsForSelection(state$1.facet(atomicRanges).map(f => f(view)), newSel);
|
|
4242
4238
|
}
|
|
4243
4239
|
view.dispatch({ selection: newSel, scrollIntoView, userEvent });
|
|
4244
4240
|
return true;
|
|
@@ -4419,6 +4415,7 @@ class InputState {
|
|
|
4419
4415
|
this.lastFocusTime = 0;
|
|
4420
4416
|
this.lastScrollTop = 0;
|
|
4421
4417
|
this.lastScrollLeft = 0;
|
|
4418
|
+
this.lastWheelEvent = 0;
|
|
4422
4419
|
// On iOS, some keys need to have their default behavior happen
|
|
4423
4420
|
// (after which we retroactively handle them and reset the DOM) to
|
|
4424
4421
|
// avoid messing up the virtual keyboard state.
|
|
@@ -4848,6 +4845,9 @@ observers.scroll = view => {
|
|
|
4848
4845
|
view.inputState.lastScrollTop = view.scrollDOM.scrollTop;
|
|
4849
4846
|
view.inputState.lastScrollLeft = view.scrollDOM.scrollLeft;
|
|
4850
4847
|
};
|
|
4848
|
+
observers.wheel = observers.mousewheel = view => {
|
|
4849
|
+
view.inputState.lastWheelEvent = Date.now();
|
|
4850
|
+
};
|
|
4851
4851
|
handlers.keydown = (view, event) => {
|
|
4852
4852
|
view.inputState.setSelectionOrigin("select");
|
|
4853
4853
|
if (event.keyCode == 27 && view.inputState.tabFocusMode != 0)
|
|
@@ -6092,7 +6092,8 @@ class LineGapWidget extends WidgetType {
|
|
|
6092
6092
|
get estimatedHeight() { return this.vertical ? this.size : -1; }
|
|
6093
6093
|
}
|
|
6094
6094
|
class ViewState {
|
|
6095
|
-
constructor(state$1) {
|
|
6095
|
+
constructor(view, state$1) {
|
|
6096
|
+
this.view = view;
|
|
6096
6097
|
this.state = state$1;
|
|
6097
6098
|
// These are contentDOM-local coordinates
|
|
6098
6099
|
this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
|
|
@@ -6103,12 +6104,14 @@ class ViewState {
|
|
|
6103
6104
|
this.contentDOMHeight = 0; // contentDOM.getBoundingClientRect().height
|
|
6104
6105
|
this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
|
|
6105
6106
|
this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
|
|
6106
|
-
this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
|
|
6107
|
-
this.scrolledToBottom = false;
|
|
6108
6107
|
// The CSS-transformation scale of the editor (transformed size /
|
|
6109
6108
|
// concrete size)
|
|
6110
6109
|
this.scaleX = 1;
|
|
6111
6110
|
this.scaleY = 1;
|
|
6111
|
+
// Last seen vertical offset of the element at the top of the scroll
|
|
6112
|
+
// container, or top of the window if there's no wrapping scroller
|
|
6113
|
+
this.scrollOffset = 0;
|
|
6114
|
+
this.scrolledToBottom = false;
|
|
6112
6115
|
// The vertical position (document-relative) to which to anchor the
|
|
6113
6116
|
// scroll position. -1 means anchor to the end of the document.
|
|
6114
6117
|
this.scrollAnchorPos = 0;
|
|
@@ -6146,6 +6149,7 @@ class ViewState {
|
|
|
6146
6149
|
this.updateViewportLines();
|
|
6147
6150
|
this.lineGaps = this.ensureLineGaps([]);
|
|
6148
6151
|
this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(this, false)));
|
|
6152
|
+
this.scrollParent = view.scrollDOM;
|
|
6149
6153
|
this.computeVisibleRanges();
|
|
6150
6154
|
}
|
|
6151
6155
|
updateForViewport() {
|
|
@@ -6179,7 +6183,7 @@ class ViewState {
|
|
|
6179
6183
|
let contentChanges = update.changedRanges;
|
|
6180
6184
|
let heightChanges = ChangedRange.extendWithRanges(contentChanges, heightRelevantDecoChanges(prevDeco, this.stateDeco, update ? update.changes : state.ChangeSet.empty(this.state.doc.length)));
|
|
6181
6185
|
let prevHeight = this.heightMap.height;
|
|
6182
|
-
let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.
|
|
6186
|
+
let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollOffset);
|
|
6183
6187
|
clearHeightChangeFlag();
|
|
6184
6188
|
this.heightMap = this.heightMap.applyChanges(this.stateDeco, update.startState.doc, this.heightOracle.setDoc(this.state.doc), heightChanges);
|
|
6185
6189
|
if (this.heightMap.height != prevHeight || heightChangeFlag)
|
|
@@ -6211,8 +6215,8 @@ class ViewState {
|
|
|
6211
6215
|
!update.state.facet(nativeSelectionHidden))
|
|
6212
6216
|
this.mustEnforceCursorAssoc = true;
|
|
6213
6217
|
}
|
|
6214
|
-
measure(
|
|
6215
|
-
let dom = view.contentDOM, style = window.getComputedStyle(dom);
|
|
6218
|
+
measure() {
|
|
6219
|
+
let { view } = this, dom = view.contentDOM, style = window.getComputedStyle(dom);
|
|
6216
6220
|
let oracle = this.heightOracle;
|
|
6217
6221
|
let whiteSpace = style.whiteSpace;
|
|
6218
6222
|
this.defaultTextDirection = style.direction == "rtl" ? exports.Direction.RTL : exports.Direction.LTR;
|
|
@@ -6246,12 +6250,18 @@ class ViewState {
|
|
|
6246
6250
|
this.editorWidth = view.scrollDOM.clientWidth;
|
|
6247
6251
|
result |= 16 /* UpdateFlag.Geometry */;
|
|
6248
6252
|
}
|
|
6249
|
-
let
|
|
6250
|
-
if (
|
|
6253
|
+
let scrollParent = scrollableParents(this.view.contentDOM, false).y;
|
|
6254
|
+
if (scrollParent != this.scrollParent) {
|
|
6255
|
+
this.scrollParent = scrollParent;
|
|
6251
6256
|
this.scrollAnchorHeight = -1;
|
|
6252
|
-
this.
|
|
6257
|
+
this.scrollOffset = 0;
|
|
6253
6258
|
}
|
|
6254
|
-
|
|
6259
|
+
let scrollOffset = this.getScrollOffset();
|
|
6260
|
+
if (this.scrollOffset != scrollOffset) {
|
|
6261
|
+
this.scrollAnchorHeight = -1;
|
|
6262
|
+
this.scrollOffset = scrollOffset;
|
|
6263
|
+
}
|
|
6264
|
+
this.scrolledToBottom = isScrolledToBottom(this.scrollParent || view.win);
|
|
6255
6265
|
// Pixel viewport
|
|
6256
6266
|
let pixelViewport = (this.printing ? fullPixelRange : visiblePixelRange)(dom, this.paddingTop);
|
|
6257
6267
|
let dTop = pixelViewport.top - this.pixelViewport.top, dBottom = pixelViewport.bottom - this.pixelViewport.bottom;
|
|
@@ -6528,9 +6538,14 @@ class ViewState {
|
|
|
6528
6538
|
this.viewportLines.find(l => l.top <= height && l.bottom >= height)) ||
|
|
6529
6539
|
scaleBlock(this.heightMap.lineAt(this.scaler.fromDOM(height), QueryType.ByHeight, this.heightOracle, 0, 0), this.scaler);
|
|
6530
6540
|
}
|
|
6531
|
-
|
|
6532
|
-
let
|
|
6533
|
-
|
|
6541
|
+
getScrollOffset() {
|
|
6542
|
+
let base = this.scrollParent == this.view.scrollDOM ? this.scrollParent.scrollTop
|
|
6543
|
+
: (this.scrollParent ? this.scrollParent.getBoundingClientRect().top : 0) - this.view.contentDOM.getBoundingClientRect().top;
|
|
6544
|
+
return base * this.scaleY;
|
|
6545
|
+
}
|
|
6546
|
+
scrollAnchorAt(scrollOffset) {
|
|
6547
|
+
let block = this.lineBlockAtHeight(scrollOffset + 8);
|
|
6548
|
+
return block.from >= this.viewport.from || this.viewportLines[0].top - scrollOffset > 200 ? block : this.viewportLines[0];
|
|
6534
6549
|
}
|
|
6535
6550
|
elementAtHeight(height) {
|
|
6536
6551
|
return scaleBlock(this.heightMap.blockAt(this.scaler.fromDOM(height), this.heightOracle, 0, 0), this.scaler);
|
|
@@ -7777,7 +7792,7 @@ class EditorView {
|
|
|
7777
7792
|
((trs) => this.update(trs));
|
|
7778
7793
|
this.dispatch = this.dispatch.bind(this);
|
|
7779
7794
|
this._root = (config.root || getRoot(config.parent) || document);
|
|
7780
|
-
this.viewState = new ViewState(config.state || state.EditorState.create(config));
|
|
7795
|
+
this.viewState = new ViewState(this, config.state || state.EditorState.create(config));
|
|
7781
7796
|
if (config.scrollTo && config.scrollTo.is(scrollIntoView))
|
|
7782
7797
|
this.viewState.scrollTarget = config.scrollTo.value.clip(this.viewState.state);
|
|
7783
7798
|
this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec));
|
|
@@ -7932,7 +7947,7 @@ class EditorView {
|
|
|
7932
7947
|
try {
|
|
7933
7948
|
for (let plugin of this.plugins)
|
|
7934
7949
|
plugin.destroy(this);
|
|
7935
|
-
this.viewState = new ViewState(newState);
|
|
7950
|
+
this.viewState = new ViewState(this, newState);
|
|
7936
7951
|
this.plugins = newState.facet(viewPlugin).map(spec => new PluginInstance(spec));
|
|
7937
7952
|
this.pluginMap.clear();
|
|
7938
7953
|
for (let plugin of this.plugins)
|
|
@@ -8011,26 +8026,26 @@ class EditorView {
|
|
|
8011
8026
|
if (flush)
|
|
8012
8027
|
this.observer.forceFlush();
|
|
8013
8028
|
let updated = null;
|
|
8014
|
-
let
|
|
8029
|
+
let scroll = this.viewState.scrollParent, scrollOffset = this.viewState.getScrollOffset();
|
|
8015
8030
|
let { scrollAnchorPos, scrollAnchorHeight } = this.viewState;
|
|
8016
|
-
if (Math.abs(
|
|
8031
|
+
if (Math.abs(scrollOffset - this.viewState.scrollOffset) > 1)
|
|
8017
8032
|
scrollAnchorHeight = -1;
|
|
8018
8033
|
this.viewState.scrollAnchorHeight = -1;
|
|
8019
8034
|
try {
|
|
8020
8035
|
for (let i = 0;; i++) {
|
|
8021
8036
|
if (scrollAnchorHeight < 0) {
|
|
8022
|
-
if (isScrolledToBottom(
|
|
8037
|
+
if (isScrolledToBottom(scroll || this.win)) {
|
|
8023
8038
|
scrollAnchorPos = -1;
|
|
8024
8039
|
scrollAnchorHeight = this.viewState.heightMap.height;
|
|
8025
8040
|
}
|
|
8026
8041
|
else {
|
|
8027
|
-
let block = this.viewState.scrollAnchorAt(
|
|
8042
|
+
let block = this.viewState.scrollAnchorAt(scrollOffset);
|
|
8028
8043
|
scrollAnchorPos = block.from;
|
|
8029
8044
|
scrollAnchorHeight = block.top;
|
|
8030
8045
|
}
|
|
8031
8046
|
}
|
|
8032
8047
|
this.updateState = 1 /* UpdateState.Measuring */;
|
|
8033
|
-
let changed = this.viewState.measure(
|
|
8048
|
+
let changed = this.viewState.measure();
|
|
8034
8049
|
if (!changed && !this.measureRequests.length && this.viewState.scrollTarget == null)
|
|
8035
8050
|
break;
|
|
8036
8051
|
if (i > 5) {
|
|
@@ -8091,10 +8106,15 @@ class EditorView {
|
|
|
8091
8106
|
else {
|
|
8092
8107
|
let newAnchorHeight = scrollAnchorPos < 0 ? this.viewState.heightMap.height :
|
|
8093
8108
|
this.viewState.lineBlockAt(scrollAnchorPos).top;
|
|
8094
|
-
let diff = newAnchorHeight - scrollAnchorHeight;
|
|
8095
|
-
if (diff > 1 || diff < -1)
|
|
8096
|
-
|
|
8097
|
-
|
|
8109
|
+
let diff = (newAnchorHeight - scrollAnchorHeight) / this.scaleY;
|
|
8110
|
+
if ((diff > 1 || diff < -1) &&
|
|
8111
|
+
(scroll == this.scrollDOM || this.hasFocus ||
|
|
8112
|
+
Math.max(this.inputState.lastWheelEvent, this.inputState.lastTouchTime) > Date.now() - 100)) {
|
|
8113
|
+
scrollOffset = scrollOffset + diff;
|
|
8114
|
+
if (scroll)
|
|
8115
|
+
scroll.scrollTop += diff;
|
|
8116
|
+
else
|
|
8117
|
+
this.win.scrollBy(0, diff);
|
|
8098
8118
|
scrollAnchorHeight = -1;
|
|
8099
8119
|
continue;
|
|
8100
8120
|
}
|
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
|
}
|
|
@@ -4153,7 +4157,7 @@ function domBoundsAround(tile, from, to, offset) {
|
|
|
4153
4157
|
}
|
|
4154
4158
|
function applyDOMChange(view, domChange) {
|
|
4155
4159
|
let change;
|
|
4156
|
-
let { newSel } = domChange, sel =
|
|
4160
|
+
let { newSel } = domChange, { state } = view, sel = state.selection.main;
|
|
4157
4161
|
let lastKey = view.inputState.lastKeyTime > Date.now() - 100 ? view.inputState.lastKeyCode : -1;
|
|
4158
4162
|
if (domChange.bounds) {
|
|
4159
4163
|
let { from, to } = domChange.bounds;
|
|
@@ -4164,8 +4168,15 @@ function applyDOMChange(view, domChange) {
|
|
|
4164
4168
|
preferredPos = sel.to;
|
|
4165
4169
|
preferredSide = "end";
|
|
4166
4170
|
}
|
|
4167
|
-
let
|
|
4168
|
-
if (
|
|
4171
|
+
let cmp = state.doc.sliceString(from, to, LineBreakPlaceholder), selEnd, diff;
|
|
4172
|
+
if (!sel.empty && sel.from >= from && sel.to <= to && (domChange.typeOver || cmp != domChange.text) &&
|
|
4173
|
+
cmp.slice(0, sel.from - from) == domChange.text.slice(0, sel.from - from) &&
|
|
4174
|
+
cmp.slice(sel.to - from) == domChange.text.slice(selEnd = domChange.text.length - (cmp.length - (sel.to - from)))) {
|
|
4175
|
+
// This looks like a selection replacement
|
|
4176
|
+
change = { from: sel.from, to: sel.to,
|
|
4177
|
+
insert: Text.of(domChange.text.slice(sel.from - from, selEnd).split(LineBreakPlaceholder)) };
|
|
4178
|
+
}
|
|
4179
|
+
else if (diff = findDiff(cmp, domChange.text, preferredPos - from, preferredSide)) {
|
|
4169
4180
|
// Chrome inserts two newlines when pressing shift-enter at the
|
|
4170
4181
|
// end of a line. DomChange drops one of those.
|
|
4171
4182
|
if (browser.chrome && lastKey == 13 &&
|
|
@@ -4175,16 +4186,12 @@ function applyDOMChange(view, domChange) {
|
|
|
4175
4186
|
insert: Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
|
|
4176
4187
|
}
|
|
4177
4188
|
}
|
|
4178
|
-
else if (newSel && (!view.hasFocus &&
|
|
4189
|
+
else if (newSel && (!view.hasFocus && state.facet(editable) || sameSelPos(newSel, sel))) {
|
|
4179
4190
|
newSel = null;
|
|
4180
4191
|
}
|
|
4181
4192
|
if (!change && !newSel)
|
|
4182
4193
|
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 &&
|
|
4194
|
+
if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
|
|
4188
4195
|
/^\. ?$/.test(change.insert.toString()) && view.contentDOM.getAttribute("autocorrect") == "off") {
|
|
4189
4196
|
// Detect insert-period-on-double-space Mac and Android behavior,
|
|
4190
4197
|
// and transform it into a regular space insert.
|
|
@@ -4192,18 +4199,7 @@ function applyDOMChange(view, domChange) {
|
|
|
4192
4199
|
newSel = EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
|
|
4193
4200
|
change = { from: change.from, to: change.to, insert: Text.of([change.insert.toString().replace(".", " ")]) };
|
|
4194
4201
|
}
|
|
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) &&
|
|
4202
|
+
else if (state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
|
|
4207
4203
|
view.inputState.insertingTextAt > Date.now() - 50) {
|
|
4208
4204
|
// For a cross-line insertion, Chrome and Safari will crudely take
|
|
4209
4205
|
// the text of the line after the selection, flattening any
|
|
@@ -4212,7 +4208,7 @@ function applyDOMChange(view, domChange) {
|
|
|
4212
4208
|
// replace of the text provided by the beforeinput event.
|
|
4213
4209
|
change = {
|
|
4214
4210
|
from: sel.from, to: sel.to,
|
|
4215
|
-
insert:
|
|
4211
|
+
insert: state.toText(view.inputState.insertingText)
|
|
4216
4212
|
};
|
|
4217
4213
|
}
|
|
4218
4214
|
else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
|
|
@@ -4234,7 +4230,7 @@ function applyDOMChange(view, domChange) {
|
|
|
4234
4230
|
scrollIntoView = true;
|
|
4235
4231
|
userEvent = view.inputState.lastSelectionOrigin;
|
|
4236
4232
|
if (userEvent == "select.pointer")
|
|
4237
|
-
newSel = skipAtomsForSelection(
|
|
4233
|
+
newSel = skipAtomsForSelection(state.facet(atomicRanges).map(f => f(view)), newSel);
|
|
4238
4234
|
}
|
|
4239
4235
|
view.dispatch({ selection: newSel, scrollIntoView, userEvent });
|
|
4240
4236
|
return true;
|
|
@@ -4415,6 +4411,7 @@ class InputState {
|
|
|
4415
4411
|
this.lastFocusTime = 0;
|
|
4416
4412
|
this.lastScrollTop = 0;
|
|
4417
4413
|
this.lastScrollLeft = 0;
|
|
4414
|
+
this.lastWheelEvent = 0;
|
|
4418
4415
|
// On iOS, some keys need to have their default behavior happen
|
|
4419
4416
|
// (after which we retroactively handle them and reset the DOM) to
|
|
4420
4417
|
// avoid messing up the virtual keyboard state.
|
|
@@ -4844,6 +4841,9 @@ observers.scroll = view => {
|
|
|
4844
4841
|
view.inputState.lastScrollTop = view.scrollDOM.scrollTop;
|
|
4845
4842
|
view.inputState.lastScrollLeft = view.scrollDOM.scrollLeft;
|
|
4846
4843
|
};
|
|
4844
|
+
observers.wheel = observers.mousewheel = view => {
|
|
4845
|
+
view.inputState.lastWheelEvent = Date.now();
|
|
4846
|
+
};
|
|
4847
4847
|
handlers.keydown = (view, event) => {
|
|
4848
4848
|
view.inputState.setSelectionOrigin("select");
|
|
4849
4849
|
if (event.keyCode == 27 && view.inputState.tabFocusMode != 0)
|
|
@@ -6087,7 +6087,8 @@ class LineGapWidget extends WidgetType {
|
|
|
6087
6087
|
get estimatedHeight() { return this.vertical ? this.size : -1; }
|
|
6088
6088
|
}
|
|
6089
6089
|
class ViewState {
|
|
6090
|
-
constructor(state) {
|
|
6090
|
+
constructor(view, state) {
|
|
6091
|
+
this.view = view;
|
|
6091
6092
|
this.state = state;
|
|
6092
6093
|
// These are contentDOM-local coordinates
|
|
6093
6094
|
this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
|
|
@@ -6098,12 +6099,14 @@ class ViewState {
|
|
|
6098
6099
|
this.contentDOMHeight = 0; // contentDOM.getBoundingClientRect().height
|
|
6099
6100
|
this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
|
|
6100
6101
|
this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
|
|
6101
|
-
this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
|
|
6102
|
-
this.scrolledToBottom = false;
|
|
6103
6102
|
// The CSS-transformation scale of the editor (transformed size /
|
|
6104
6103
|
// concrete size)
|
|
6105
6104
|
this.scaleX = 1;
|
|
6106
6105
|
this.scaleY = 1;
|
|
6106
|
+
// Last seen vertical offset of the element at the top of the scroll
|
|
6107
|
+
// container, or top of the window if there's no wrapping scroller
|
|
6108
|
+
this.scrollOffset = 0;
|
|
6109
|
+
this.scrolledToBottom = false;
|
|
6107
6110
|
// The vertical position (document-relative) to which to anchor the
|
|
6108
6111
|
// scroll position. -1 means anchor to the end of the document.
|
|
6109
6112
|
this.scrollAnchorPos = 0;
|
|
@@ -6141,6 +6144,7 @@ class ViewState {
|
|
|
6141
6144
|
this.updateViewportLines();
|
|
6142
6145
|
this.lineGaps = this.ensureLineGaps([]);
|
|
6143
6146
|
this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(this, false)));
|
|
6147
|
+
this.scrollParent = view.scrollDOM;
|
|
6144
6148
|
this.computeVisibleRanges();
|
|
6145
6149
|
}
|
|
6146
6150
|
updateForViewport() {
|
|
@@ -6174,7 +6178,7 @@ class ViewState {
|
|
|
6174
6178
|
let contentChanges = update.changedRanges;
|
|
6175
6179
|
let heightChanges = ChangedRange.extendWithRanges(contentChanges, heightRelevantDecoChanges(prevDeco, this.stateDeco, update ? update.changes : ChangeSet.empty(this.state.doc.length)));
|
|
6176
6180
|
let prevHeight = this.heightMap.height;
|
|
6177
|
-
let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.
|
|
6181
|
+
let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollOffset);
|
|
6178
6182
|
clearHeightChangeFlag();
|
|
6179
6183
|
this.heightMap = this.heightMap.applyChanges(this.stateDeco, update.startState.doc, this.heightOracle.setDoc(this.state.doc), heightChanges);
|
|
6180
6184
|
if (this.heightMap.height != prevHeight || heightChangeFlag)
|
|
@@ -6206,8 +6210,8 @@ class ViewState {
|
|
|
6206
6210
|
!update.state.facet(nativeSelectionHidden))
|
|
6207
6211
|
this.mustEnforceCursorAssoc = true;
|
|
6208
6212
|
}
|
|
6209
|
-
measure(
|
|
6210
|
-
let dom = view.contentDOM, style = window.getComputedStyle(dom);
|
|
6213
|
+
measure() {
|
|
6214
|
+
let { view } = this, dom = view.contentDOM, style = window.getComputedStyle(dom);
|
|
6211
6215
|
let oracle = this.heightOracle;
|
|
6212
6216
|
let whiteSpace = style.whiteSpace;
|
|
6213
6217
|
this.defaultTextDirection = style.direction == "rtl" ? Direction.RTL : Direction.LTR;
|
|
@@ -6241,12 +6245,18 @@ class ViewState {
|
|
|
6241
6245
|
this.editorWidth = view.scrollDOM.clientWidth;
|
|
6242
6246
|
result |= 16 /* UpdateFlag.Geometry */;
|
|
6243
6247
|
}
|
|
6244
|
-
let
|
|
6245
|
-
if (
|
|
6248
|
+
let scrollParent = scrollableParents(this.view.contentDOM, false).y;
|
|
6249
|
+
if (scrollParent != this.scrollParent) {
|
|
6250
|
+
this.scrollParent = scrollParent;
|
|
6246
6251
|
this.scrollAnchorHeight = -1;
|
|
6247
|
-
this.
|
|
6252
|
+
this.scrollOffset = 0;
|
|
6248
6253
|
}
|
|
6249
|
-
|
|
6254
|
+
let scrollOffset = this.getScrollOffset();
|
|
6255
|
+
if (this.scrollOffset != scrollOffset) {
|
|
6256
|
+
this.scrollAnchorHeight = -1;
|
|
6257
|
+
this.scrollOffset = scrollOffset;
|
|
6258
|
+
}
|
|
6259
|
+
this.scrolledToBottom = isScrolledToBottom(this.scrollParent || view.win);
|
|
6250
6260
|
// Pixel viewport
|
|
6251
6261
|
let pixelViewport = (this.printing ? fullPixelRange : visiblePixelRange)(dom, this.paddingTop);
|
|
6252
6262
|
let dTop = pixelViewport.top - this.pixelViewport.top, dBottom = pixelViewport.bottom - this.pixelViewport.bottom;
|
|
@@ -6523,9 +6533,14 @@ class ViewState {
|
|
|
6523
6533
|
this.viewportLines.find(l => l.top <= height && l.bottom >= height)) ||
|
|
6524
6534
|
scaleBlock(this.heightMap.lineAt(this.scaler.fromDOM(height), QueryType.ByHeight, this.heightOracle, 0, 0), this.scaler);
|
|
6525
6535
|
}
|
|
6526
|
-
|
|
6527
|
-
let
|
|
6528
|
-
|
|
6536
|
+
getScrollOffset() {
|
|
6537
|
+
let base = this.scrollParent == this.view.scrollDOM ? this.scrollParent.scrollTop
|
|
6538
|
+
: (this.scrollParent ? this.scrollParent.getBoundingClientRect().top : 0) - this.view.contentDOM.getBoundingClientRect().top;
|
|
6539
|
+
return base * this.scaleY;
|
|
6540
|
+
}
|
|
6541
|
+
scrollAnchorAt(scrollOffset) {
|
|
6542
|
+
let block = this.lineBlockAtHeight(scrollOffset + 8);
|
|
6543
|
+
return block.from >= this.viewport.from || this.viewportLines[0].top - scrollOffset > 200 ? block : this.viewportLines[0];
|
|
6529
6544
|
}
|
|
6530
6545
|
elementAtHeight(height) {
|
|
6531
6546
|
return scaleBlock(this.heightMap.blockAt(this.scaler.fromDOM(height), this.heightOracle, 0, 0), this.scaler);
|
|
@@ -7772,7 +7787,7 @@ class EditorView {
|
|
|
7772
7787
|
((trs) => this.update(trs));
|
|
7773
7788
|
this.dispatch = this.dispatch.bind(this);
|
|
7774
7789
|
this._root = (config.root || getRoot(config.parent) || document);
|
|
7775
|
-
this.viewState = new ViewState(config.state || EditorState.create(config));
|
|
7790
|
+
this.viewState = new ViewState(this, config.state || EditorState.create(config));
|
|
7776
7791
|
if (config.scrollTo && config.scrollTo.is(scrollIntoView))
|
|
7777
7792
|
this.viewState.scrollTarget = config.scrollTo.value.clip(this.viewState.state);
|
|
7778
7793
|
this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec));
|
|
@@ -7927,7 +7942,7 @@ class EditorView {
|
|
|
7927
7942
|
try {
|
|
7928
7943
|
for (let plugin of this.plugins)
|
|
7929
7944
|
plugin.destroy(this);
|
|
7930
|
-
this.viewState = new ViewState(newState);
|
|
7945
|
+
this.viewState = new ViewState(this, newState);
|
|
7931
7946
|
this.plugins = newState.facet(viewPlugin).map(spec => new PluginInstance(spec));
|
|
7932
7947
|
this.pluginMap.clear();
|
|
7933
7948
|
for (let plugin of this.plugins)
|
|
@@ -8006,26 +8021,26 @@ class EditorView {
|
|
|
8006
8021
|
if (flush)
|
|
8007
8022
|
this.observer.forceFlush();
|
|
8008
8023
|
let updated = null;
|
|
8009
|
-
let
|
|
8024
|
+
let scroll = this.viewState.scrollParent, scrollOffset = this.viewState.getScrollOffset();
|
|
8010
8025
|
let { scrollAnchorPos, scrollAnchorHeight } = this.viewState;
|
|
8011
|
-
if (Math.abs(
|
|
8026
|
+
if (Math.abs(scrollOffset - this.viewState.scrollOffset) > 1)
|
|
8012
8027
|
scrollAnchorHeight = -1;
|
|
8013
8028
|
this.viewState.scrollAnchorHeight = -1;
|
|
8014
8029
|
try {
|
|
8015
8030
|
for (let i = 0;; i++) {
|
|
8016
8031
|
if (scrollAnchorHeight < 0) {
|
|
8017
|
-
if (isScrolledToBottom(
|
|
8032
|
+
if (isScrolledToBottom(scroll || this.win)) {
|
|
8018
8033
|
scrollAnchorPos = -1;
|
|
8019
8034
|
scrollAnchorHeight = this.viewState.heightMap.height;
|
|
8020
8035
|
}
|
|
8021
8036
|
else {
|
|
8022
|
-
let block = this.viewState.scrollAnchorAt(
|
|
8037
|
+
let block = this.viewState.scrollAnchorAt(scrollOffset);
|
|
8023
8038
|
scrollAnchorPos = block.from;
|
|
8024
8039
|
scrollAnchorHeight = block.top;
|
|
8025
8040
|
}
|
|
8026
8041
|
}
|
|
8027
8042
|
this.updateState = 1 /* UpdateState.Measuring */;
|
|
8028
|
-
let changed = this.viewState.measure(
|
|
8043
|
+
let changed = this.viewState.measure();
|
|
8029
8044
|
if (!changed && !this.measureRequests.length && this.viewState.scrollTarget == null)
|
|
8030
8045
|
break;
|
|
8031
8046
|
if (i > 5) {
|
|
@@ -8086,10 +8101,15 @@ class EditorView {
|
|
|
8086
8101
|
else {
|
|
8087
8102
|
let newAnchorHeight = scrollAnchorPos < 0 ? this.viewState.heightMap.height :
|
|
8088
8103
|
this.viewState.lineBlockAt(scrollAnchorPos).top;
|
|
8089
|
-
let diff = newAnchorHeight - scrollAnchorHeight;
|
|
8090
|
-
if (diff > 1 || diff < -1)
|
|
8091
|
-
|
|
8092
|
-
|
|
8104
|
+
let diff = (newAnchorHeight - scrollAnchorHeight) / this.scaleY;
|
|
8105
|
+
if ((diff > 1 || diff < -1) &&
|
|
8106
|
+
(scroll == this.scrollDOM || this.hasFocus ||
|
|
8107
|
+
Math.max(this.inputState.lastWheelEvent, this.inputState.lastTouchTime) > Date.now() - 100)) {
|
|
8108
|
+
scrollOffset = scrollOffset + diff;
|
|
8109
|
+
if (scroll)
|
|
8110
|
+
scroll.scrollTop += diff;
|
|
8111
|
+
else
|
|
8112
|
+
this.win.scrollBy(0, diff);
|
|
8093
8113
|
scrollAnchorHeight = -1;
|
|
8094
8114
|
continue;
|
|
8095
8115
|
}
|