prosemirror-view 1.29.1 → 1.30.0
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 +16 -0
- package/dist/index.cjs +29 -8
- package/dist/index.d.ts +56 -1
- package/dist/index.js +31 -11
- package/package.json +1 -1
- package/src/#domcoords.ts# +482 -0
- package/src/domchange.ts +4 -0
- package/src/domcoords.ts +2 -3
- package/src/index.ts +13 -1
- package/src/input.ts +10 -8
- package/src/viewdesc.ts +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 1.30.0 (2023-01-19)
|
|
2
|
+
|
|
3
|
+
### New features
|
|
4
|
+
|
|
5
|
+
`EditorView` now has `pasteHTML` and `pasteText` methods which initiate paste handling.
|
|
6
|
+
|
|
7
|
+
## 1.29.2 (2023-01-17)
|
|
8
|
+
|
|
9
|
+
### Bug fixes
|
|
10
|
+
|
|
11
|
+
Make sure enter key handlers are called on Chrome Android when the cursor is in a position where the native behavior of Enter doesn't change the document.
|
|
12
|
+
|
|
13
|
+
Fix a crash that could occur when starting a composition at the end of a non-inclusive mark.
|
|
14
|
+
|
|
15
|
+
Fix a bug in (vertical) `endOfTextblock` that caused it to return falsely positive results inside textblocks that had a separate `contentDOM`.
|
|
16
|
+
|
|
1
17
|
## 1.29.1 (2022-11-14)
|
|
2
18
|
|
|
3
19
|
### Bug fixes
|
package/dist/index.cjs
CHANGED
|
@@ -628,7 +628,7 @@ function endOfTextblockVertical(view, state, dir) {
|
|
|
628
628
|
if (!nearest) break;
|
|
629
629
|
|
|
630
630
|
if (nearest.node.isBlock) {
|
|
631
|
-
dom = nearest.dom;
|
|
631
|
+
dom = nearest.contentDOM || nearest.dom;
|
|
632
632
|
break;
|
|
633
633
|
}
|
|
634
634
|
|
|
@@ -2045,7 +2045,9 @@ var ViewTreeUpdater = function () {
|
|
|
2045
2045
|
var found = -1;
|
|
2046
2046
|
|
|
2047
2047
|
for (var i = this.index; i < Math.min(this.index + 3, this.top.children.length); i++) {
|
|
2048
|
-
|
|
2048
|
+
var next = this.top.children[i];
|
|
2049
|
+
|
|
2050
|
+
if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) {
|
|
2049
2051
|
found = i;
|
|
2050
2052
|
break;
|
|
2051
2053
|
}
|
|
@@ -2132,7 +2134,7 @@ var ViewTreeUpdater = function () {
|
|
|
2132
2134
|
|
|
2133
2135
|
if (_preMatch != null && _preMatch != index) return false;
|
|
2134
2136
|
var nextDOM = next.dom;
|
|
2135
|
-
var locked = this.
|
|
2137
|
+
var locked = this.isLocked(nextDOM) && !(node.isText && next.node && next.node.isText && next.nodeDOM.nodeValue == node.text && next.dirty != NODE_DIRTY && sameOuterDeco(outerDeco, next.outerDeco));
|
|
2136
2138
|
|
|
2137
2139
|
if (!locked && next.update(node, outerDeco, innerDeco, view)) {
|
|
2138
2140
|
this.destroyBetween(this.index, i);
|
|
@@ -2201,6 +2203,11 @@ var ViewTreeUpdater = function () {
|
|
|
2201
2203
|
this.changed = true;
|
|
2202
2204
|
}
|
|
2203
2205
|
}
|
|
2206
|
+
}, {
|
|
2207
|
+
key: "isLocked",
|
|
2208
|
+
value: function isLocked(node) {
|
|
2209
|
+
return this.lock && (node == this.lock || node.nodeType == 1 && node.contains(this.lock.parentNode));
|
|
2210
|
+
}
|
|
2204
2211
|
}]);
|
|
2205
2212
|
|
|
2206
2213
|
return ViewTreeUpdater;
|
|
@@ -3346,7 +3353,7 @@ editHandlers.keypress = function (view, _event) {
|
|
|
3346
3353
|
|
|
3347
3354
|
if (!(sel instanceof prosemirrorState.TextSelection) || !sel.$from.sameParent(sel.$to)) {
|
|
3348
3355
|
var text = String.fromCharCode(event.charCode);
|
|
3349
|
-
if (!view.someProp("handleTextInput", function (f) {
|
|
3356
|
+
if (!/[\r\n]/.test(text) && !view.someProp("handleTextInput", function (f) {
|
|
3350
3357
|
return f(view, sel.$from.pos, sel.$to.pos, text);
|
|
3351
3358
|
})) view.dispatch(view.state.tr.insertText(text).scrollIntoView());
|
|
3352
3359
|
event.preventDefault();
|
|
@@ -3788,12 +3795,12 @@ function capturePaste(view, event) {
|
|
|
3788
3795
|
setTimeout(function () {
|
|
3789
3796
|
view.focus();
|
|
3790
3797
|
if (target.parentNode) target.parentNode.removeChild(target);
|
|
3791
|
-
if (plainText) doPaste(view, target.value, null, event);else doPaste(view, target.textContent, target.innerHTML, event);
|
|
3798
|
+
if (plainText) doPaste(view, target.value, null, view.input.shiftKey, event);else doPaste(view, target.textContent, target.innerHTML, view.input.shiftKey, event);
|
|
3792
3799
|
}, 50);
|
|
3793
3800
|
}
|
|
3794
3801
|
|
|
3795
|
-
function doPaste(view, text, html, event) {
|
|
3796
|
-
var slice = parseFromClipboard(view, text, html,
|
|
3802
|
+
function doPaste(view, text, html, preferPlain, event) {
|
|
3803
|
+
var slice = parseFromClipboard(view, text, html, preferPlain, view.state.selection.$from);
|
|
3797
3804
|
if (view.someProp("handlePaste", function (f) {
|
|
3798
3805
|
return f(view, event, slice || prosemirrorModel.Slice.empty);
|
|
3799
3806
|
})) return true;
|
|
@@ -3808,7 +3815,7 @@ editHandlers.paste = function (view, _event) {
|
|
|
3808
3815
|
var event = _event;
|
|
3809
3816
|
if (view.composing && !android) return;
|
|
3810
3817
|
var data = brokenClipboardAPI ? null : event.clipboardData;
|
|
3811
|
-
if (data && doPaste(view, data.getData("text/plain"), data.getData("text/html"), event)) event.preventDefault();else capturePaste(view, event);
|
|
3818
|
+
if (data && doPaste(view, data.getData("text/plain"), data.getData("text/html"), view.input.shiftKey, event)) event.preventDefault();else capturePaste(view, event);
|
|
3812
3819
|
};
|
|
3813
3820
|
|
|
3814
3821
|
var Dragging = _createClass(function Dragging(slice, move) {
|
|
@@ -5160,6 +5167,10 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
|
|
|
5160
5167
|
var newSel = selectionFromDOM(view, origin);
|
|
5161
5168
|
|
|
5162
5169
|
if (newSel && !view.state.selection.eq(newSel)) {
|
|
5170
|
+
if (chrome && android && view.input.lastKeyCode === 13 && Date.now() - 100 < view.input.lastKeyCodeTime && view.someProp("handleKeyDown", function (f) {
|
|
5171
|
+
return f(view, keyEvent(13, "Enter"));
|
|
5172
|
+
})) return;
|
|
5173
|
+
|
|
5163
5174
|
var _tr = view.state.tr.setSelection(newSel);
|
|
5164
5175
|
|
|
5165
5176
|
if (origin == "pointer") _tr.setMeta("pointer", true);else if (origin == "key") _tr.scrollIntoView();
|
|
@@ -5761,6 +5772,16 @@ var EditorView = function () {
|
|
|
5761
5772
|
value: function endOfTextblock(dir, state) {
|
|
5762
5773
|
return _endOfTextblock(this, state || this.state, dir);
|
|
5763
5774
|
}
|
|
5775
|
+
}, {
|
|
5776
|
+
key: "pasteHTML",
|
|
5777
|
+
value: function pasteHTML(html, event) {
|
|
5778
|
+
return doPaste(this, "", html, false, event || new ClipboardEvent("paste"));
|
|
5779
|
+
}
|
|
5780
|
+
}, {
|
|
5781
|
+
key: "pasteText",
|
|
5782
|
+
value: function pasteText(text, event) {
|
|
5783
|
+
return doPaste(this, text, null, true, event || new ClipboardEvent("paste"));
|
|
5784
|
+
}
|
|
5764
5785
|
}, {
|
|
5765
5786
|
key: "destroy",
|
|
5766
5787
|
value: function destroy() {
|
package/dist/index.d.ts
CHANGED
|
@@ -319,7 +319,8 @@ declare class ViewDesc {
|
|
|
319
319
|
get posAfter(): number;
|
|
320
320
|
get posAtEnd(): number;
|
|
321
321
|
localPosFromDOM(dom: DOMNode, offset: number, bias: number): number;
|
|
322
|
-
nearestDesc(dom: DOMNode
|
|
322
|
+
nearestDesc(dom: DOMNode): ViewDesc | undefined;
|
|
323
|
+
nearestDesc(dom: DOMNode, onlyNodes: true): NodeViewDesc | undefined;
|
|
323
324
|
getDesc(dom: DOMNode): ViewDesc | undefined;
|
|
324
325
|
posFromDOM(dom: DOMNode, offset: number, bias: number): number;
|
|
325
326
|
descAt(pos: number): ViewDesc | undefined;
|
|
@@ -345,6 +346,50 @@ declare class ViewDesc {
|
|
|
345
346
|
get domAtom(): boolean;
|
|
346
347
|
get ignoreForCoords(): boolean;
|
|
347
348
|
}
|
|
349
|
+
declare class NodeViewDesc extends ViewDesc {
|
|
350
|
+
node: Node;
|
|
351
|
+
outerDeco: readonly Decoration[];
|
|
352
|
+
innerDeco: DecorationSource;
|
|
353
|
+
readonly nodeDOM: DOMNode;
|
|
354
|
+
constructor(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, dom: DOMNode, contentDOM: HTMLElement | null, nodeDOM: DOMNode, view: EditorView, pos: number);
|
|
355
|
+
static create(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView, pos: number): NodeViewDesc | TextViewDesc;
|
|
356
|
+
parseRule(): ParseRule | null;
|
|
357
|
+
matchesNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource): boolean;
|
|
358
|
+
get size(): number;
|
|
359
|
+
get border(): 1 | 0;
|
|
360
|
+
updateChildren(view: EditorView, pos: number): void;
|
|
361
|
+
localCompositionInfo(view: EditorView, pos: number): {
|
|
362
|
+
node: Text;
|
|
363
|
+
pos: number;
|
|
364
|
+
text: string;
|
|
365
|
+
} | null;
|
|
366
|
+
protectLocalComposition(view: EditorView, { node, pos, text }: {
|
|
367
|
+
node: Text;
|
|
368
|
+
pos: number;
|
|
369
|
+
text: string;
|
|
370
|
+
}): void;
|
|
371
|
+
update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView): boolean;
|
|
372
|
+
updateInner(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView): void;
|
|
373
|
+
updateOuterDeco(outerDeco: readonly Decoration[]): void;
|
|
374
|
+
selectNode(): void;
|
|
375
|
+
deselectNode(): void;
|
|
376
|
+
get domAtom(): boolean;
|
|
377
|
+
}
|
|
378
|
+
declare class TextViewDesc extends NodeViewDesc {
|
|
379
|
+
constructor(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, dom: DOMNode, nodeDOM: DOMNode, view: EditorView);
|
|
380
|
+
parseRule(): ParseRule;
|
|
381
|
+
update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView): boolean;
|
|
382
|
+
inParent(): boolean;
|
|
383
|
+
domFromPos(pos: number): {
|
|
384
|
+
node: globalThis.Node;
|
|
385
|
+
offset: number;
|
|
386
|
+
};
|
|
387
|
+
localPosFromDOM(dom: DOMNode, offset: number, bias: number): number;
|
|
388
|
+
ignoreMutation(mutation: MutationRecord): boolean;
|
|
389
|
+
slice(from: number, to: number, view: EditorView): TextViewDesc;
|
|
390
|
+
markDirty(from: number, to: number): void;
|
|
391
|
+
get domAtom(): boolean;
|
|
392
|
+
}
|
|
348
393
|
|
|
349
394
|
/**
|
|
350
395
|
An editor view manages the DOM structure that represents an
|
|
@@ -520,6 +565,16 @@ declare class EditorView {
|
|
|
520
565
|
*/
|
|
521
566
|
endOfTextblock(dir: "up" | "down" | "left" | "right" | "forward" | "backward", state?: EditorState): boolean;
|
|
522
567
|
/**
|
|
568
|
+
Run the editor's paste logic with the given HTML string. The
|
|
569
|
+
`event`, if given, will be passed to the
|
|
570
|
+
[`handlePaste`](https://prosemirror.net/docs/ref/#view.EditorProps.handlePaste) hook.
|
|
571
|
+
*/
|
|
572
|
+
pasteHTML(html: string, event?: ClipboardEvent): boolean;
|
|
573
|
+
/**
|
|
574
|
+
Run the editor's paste logic with the given plain-text input.
|
|
575
|
+
*/
|
|
576
|
+
pasteText(text: string, event?: ClipboardEvent): boolean;
|
|
577
|
+
/**
|
|
523
578
|
Removes the editor from the DOM and destroys all [node
|
|
524
579
|
views](https://prosemirror.net/docs/ref/#view.NodeView).
|
|
525
580
|
*/
|
package/dist/index.js
CHANGED
|
@@ -541,7 +541,7 @@ function endOfTextblockVertical(view, state, dir) {
|
|
|
541
541
|
if (!nearest)
|
|
542
542
|
break;
|
|
543
543
|
if (nearest.node.isBlock) {
|
|
544
|
-
dom = nearest.dom;
|
|
544
|
+
dom = nearest.contentDOM || nearest.dom;
|
|
545
545
|
break;
|
|
546
546
|
}
|
|
547
547
|
dom = nearest.dom.parentNode;
|
|
@@ -760,8 +760,6 @@ class ViewDesc {
|
|
|
760
760
|
}
|
|
761
761
|
return (atEnd == null ? bias > 0 : atEnd) ? this.posAtEnd : this.posAtStart;
|
|
762
762
|
}
|
|
763
|
-
// Scan up the dom finding the first desc that is a descendant of
|
|
764
|
-
// this one.
|
|
765
763
|
nearestDesc(dom, onlyNodes = false) {
|
|
766
764
|
for (let first = true, cur = dom; cur; cur = cur.parentNode) {
|
|
767
765
|
let desc = this.getDesc(cur), nodeDOM;
|
|
@@ -1679,7 +1677,8 @@ class ViewTreeUpdater {
|
|
|
1679
1677
|
this.stack.push(this.top, this.index + 1);
|
|
1680
1678
|
let found = -1;
|
|
1681
1679
|
for (let i = this.index; i < Math.min(this.index + 3, this.top.children.length); i++) {
|
|
1682
|
-
|
|
1680
|
+
let next = this.top.children[i];
|
|
1681
|
+
if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) {
|
|
1683
1682
|
found = i;
|
|
1684
1683
|
break;
|
|
1685
1684
|
}
|
|
@@ -1765,7 +1764,7 @@ class ViewTreeUpdater {
|
|
|
1765
1764
|
// Can't update if nextDOM is or contains this.lock, except if
|
|
1766
1765
|
// it's a text node whose content already matches the new text
|
|
1767
1766
|
// and whose decorations match the new ones.
|
|
1768
|
-
let locked = this.
|
|
1767
|
+
let locked = this.isLocked(nextDOM) &&
|
|
1769
1768
|
!(node.isText && next.node && next.node.isText && next.nodeDOM.nodeValue == node.text &&
|
|
1770
1769
|
next.dirty != NODE_DIRTY && sameOuterDeco(outerDeco, next.outerDeco));
|
|
1771
1770
|
if (!locked && next.update(node, outerDeco, innerDeco, view)) {
|
|
@@ -1835,6 +1834,9 @@ class ViewTreeUpdater {
|
|
|
1835
1834
|
this.changed = true;
|
|
1836
1835
|
}
|
|
1837
1836
|
}
|
|
1837
|
+
isLocked(node) {
|
|
1838
|
+
return this.lock && (node == this.lock || node.nodeType == 1 && node.contains(this.lock.parentNode));
|
|
1839
|
+
}
|
|
1838
1840
|
}
|
|
1839
1841
|
// Iterate from the end of the fragment and array of descs to find
|
|
1840
1842
|
// directly matching ones, in order to avoid overeagerly reusing those
|
|
@@ -2921,7 +2923,7 @@ editHandlers.keypress = (view, _event) => {
|
|
|
2921
2923
|
let sel = view.state.selection;
|
|
2922
2924
|
if (!(sel instanceof TextSelection) || !sel.$from.sameParent(sel.$to)) {
|
|
2923
2925
|
let text = String.fromCharCode(event.charCode);
|
|
2924
|
-
if (!view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text)))
|
|
2926
|
+
if (!/[\r\n]/.test(text) && !view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text)))
|
|
2925
2927
|
view.dispatch(view.state.tr.insertText(text).scrollIntoView());
|
|
2926
2928
|
event.preventDefault();
|
|
2927
2929
|
}
|
|
@@ -3341,13 +3343,13 @@ function capturePaste(view, event) {
|
|
|
3341
3343
|
if (target.parentNode)
|
|
3342
3344
|
target.parentNode.removeChild(target);
|
|
3343
3345
|
if (plainText)
|
|
3344
|
-
doPaste(view, target.value, null, event);
|
|
3346
|
+
doPaste(view, target.value, null, view.input.shiftKey, event);
|
|
3345
3347
|
else
|
|
3346
|
-
doPaste(view, target.textContent, target.innerHTML, event);
|
|
3348
|
+
doPaste(view, target.textContent, target.innerHTML, view.input.shiftKey, event);
|
|
3347
3349
|
}, 50);
|
|
3348
3350
|
}
|
|
3349
|
-
function doPaste(view, text, html, event) {
|
|
3350
|
-
let slice = parseFromClipboard(view, text, html,
|
|
3351
|
+
function doPaste(view, text, html, preferPlain, event) {
|
|
3352
|
+
let slice = parseFromClipboard(view, text, html, preferPlain, view.state.selection.$from);
|
|
3351
3353
|
if (view.someProp("handlePaste", f => f(view, event, slice || Slice.empty)))
|
|
3352
3354
|
return true;
|
|
3353
3355
|
if (!slice)
|
|
@@ -3368,7 +3370,7 @@ editHandlers.paste = (view, _event) => {
|
|
|
3368
3370
|
if (view.composing && !android)
|
|
3369
3371
|
return;
|
|
3370
3372
|
let data = brokenClipboardAPI ? null : event.clipboardData;
|
|
3371
|
-
if (data && doPaste(view, data.getData("text/plain"), data.getData("text/html"), event))
|
|
3373
|
+
if (data && doPaste(view, data.getData("text/plain"), data.getData("text/html"), view.input.shiftKey, event))
|
|
3372
3374
|
event.preventDefault();
|
|
3373
3375
|
else
|
|
3374
3376
|
capturePaste(view, event);
|
|
@@ -4571,6 +4573,10 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
|
|
|
4571
4573
|
let origin = view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null;
|
|
4572
4574
|
let newSel = selectionFromDOM(view, origin);
|
|
4573
4575
|
if (newSel && !view.state.selection.eq(newSel)) {
|
|
4576
|
+
if (chrome && android &&
|
|
4577
|
+
view.input.lastKeyCode === 13 && Date.now() - 100 < view.input.lastKeyCodeTime &&
|
|
4578
|
+
view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter"))))
|
|
4579
|
+
return;
|
|
4574
4580
|
let tr = view.state.tr.setSelection(newSel);
|
|
4575
4581
|
if (origin == "pointer")
|
|
4576
4582
|
tr.setMeta("pointer", true);
|
|
@@ -5254,6 +5260,20 @@ class EditorView {
|
|
|
5254
5260
|
return endOfTextblock(this, state || this.state, dir);
|
|
5255
5261
|
}
|
|
5256
5262
|
/**
|
|
5263
|
+
Run the editor's paste logic with the given HTML string. The
|
|
5264
|
+
`event`, if given, will be passed to the
|
|
5265
|
+
[`handlePaste`](https://prosemirror.net/docs/ref/#view.EditorProps.handlePaste) hook.
|
|
5266
|
+
*/
|
|
5267
|
+
pasteHTML(html, event) {
|
|
5268
|
+
return doPaste(this, "", html, false, event || new ClipboardEvent("paste"));
|
|
5269
|
+
}
|
|
5270
|
+
/**
|
|
5271
|
+
Run the editor's paste logic with the given plain-text input.
|
|
5272
|
+
*/
|
|
5273
|
+
pasteText(text, event) {
|
|
5274
|
+
return doPaste(this, text, null, true, event || new ClipboardEvent("paste"));
|
|
5275
|
+
}
|
|
5276
|
+
/**
|
|
5257
5277
|
Removes the editor from the DOM and destroys all [node
|
|
5258
5278
|
views](https://prosemirror.net/docs/ref/#view.NodeView).
|
|
5259
5279
|
*/
|
package/package.json
CHANGED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import {EditorState} from "prosemirror-state"
|
|
2
|
+
import {nodeSize, textRange, parentNode} from "./dom"
|
|
3
|
+
import * as browser from "./browser"
|
|
4
|
+
import {EditorView} from "./index"
|
|
5
|
+
|
|
6
|
+
export type Rect = {left: number, right: number, top: number, bottom: number}
|
|
7
|
+
|
|
8
|
+
function windowRect(doc: Document): Rect {
|
|
9
|
+
return {left: 0, right: doc.documentElement.clientWidth,
|
|
10
|
+
top: 0, bottom: doc.documentElement.clientHeight}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getSide(value: number | Rect, side: keyof Rect): number {
|
|
14
|
+
return typeof value == "number" ? value : value[side]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function clientRect(node: HTMLElement): Rect {
|
|
18
|
+
let rect = node.getBoundingClientRect()
|
|
19
|
+
// Adjust for elements with style "transform: scale()"
|
|
20
|
+
let scaleX = (rect.width / node.offsetWidth) || 1
|
|
21
|
+
let scaleY = (rect.height / node.offsetHeight) || 1
|
|
22
|
+
// Make sure scrollbar width isn't included in the rectangle
|
|
23
|
+
return {left: rect.left, right: rect.left + node.clientWidth * scaleX,
|
|
24
|
+
top: rect.top, bottom: rect.top + node.clientHeight * scaleY}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function scrollRectIntoView(view: EditorView, rect: Rect, startDOM: Node) {
|
|
28
|
+
let scrollThreshold = view.someProp("scrollThreshold") || 0, scrollMargin = view.someProp("scrollMargin") || 5
|
|
29
|
+
let doc = view.dom.ownerDocument
|
|
30
|
+
for (let parent: Node | null = startDOM || view.dom;; parent = parentNode(parent)) {
|
|
31
|
+
if (!parent) break
|
|
32
|
+
if (parent.nodeType != 1) continue
|
|
33
|
+
let elt = parent as HTMLElement
|
|
34
|
+
let atTop = elt == doc.body
|
|
35
|
+
let bounding = atTop ? windowRect(doc) : clientRect(elt as HTMLElement)
|
|
36
|
+
let moveX = 0, moveY = 0
|
|
37
|
+
if (rect.top < bounding.top + getSide(scrollThreshold, "top"))
|
|
38
|
+
moveY = -(bounding.top - rect.top + getSide(scrollMargin, "top"))
|
|
39
|
+
else if (rect.bottom > bounding.bottom - getSide(scrollThreshold, "bottom"))
|
|
40
|
+
moveY = rect.bottom - bounding.bottom + getSide(scrollMargin, "bottom")
|
|
41
|
+
if (rect.left < bounding.left + getSide(scrollThreshold, "left"))
|
|
42
|
+
moveX = -(bounding.left - rect.left + getSide(scrollMargin, "left"))
|
|
43
|
+
else if (rect.right > bounding.right - getSide(scrollThreshold, "right"))
|
|
44
|
+
moveX = rect.right - bounding.right + getSide(scrollMargin, "right")
|
|
45
|
+
if (moveX || moveY) {
|
|
46
|
+
if (atTop) {
|
|
47
|
+
doc.defaultView!.scrollBy(moveX, moveY)
|
|
48
|
+
} else {
|
|
49
|
+
let startX = elt.scrollLeft, startY = elt.scrollTop
|
|
50
|
+
if (moveY) elt.scrollTop += moveY
|
|
51
|
+
if (moveX) elt.scrollLeft += moveX
|
|
52
|
+
let dX = elt.scrollLeft - startX, dY = elt.scrollTop - startY
|
|
53
|
+
rect = {left: rect.left - dX, top: rect.top - dY, right: rect.right - dX, bottom: rect.bottom - dY}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (atTop) break
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Store the scroll position of the editor's parent nodes, along with
|
|
61
|
+
// the top position of an element near the top of the editor, which
|
|
62
|
+
// will be used to make sure the visible viewport remains stable even
|
|
63
|
+
// when the size of the content above changes.
|
|
64
|
+
export function storeScrollPos(view: EditorView): {
|
|
65
|
+
refDOM: HTMLElement,
|
|
66
|
+
refTop: number,
|
|
67
|
+
stack: {dom: HTMLElement, top: number, left: number}[]
|
|
68
|
+
} {
|
|
69
|
+
let rect = view.dom.getBoundingClientRect(), startY = Math.max(0, rect.top)
|
|
70
|
+
let refDOM: HTMLElement, refTop: number
|
|
71
|
+
for (let x = (rect.left + rect.right) / 2, y = startY + 1;
|
|
72
|
+
y < Math.min(innerHeight, rect.bottom); y += 5) {
|
|
73
|
+
let dom = view.root.elementFromPoint(x, y)
|
|
74
|
+
if (!dom || dom == view.dom || !view.dom.contains(dom)) continue
|
|
75
|
+
let localRect = (dom as HTMLElement).getBoundingClientRect()
|
|
76
|
+
if (localRect.top >= startY - 20) {
|
|
77
|
+
refDOM = dom as HTMLElement
|
|
78
|
+
refTop = localRect.top
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {refDOM: refDOM!, refTop: refTop!, stack: scrollStack(view.dom)}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function scrollStack(dom: Node): {dom: HTMLElement, top: number, left: number}[] {
|
|
86
|
+
let stack = [], doc = dom.ownerDocument
|
|
87
|
+
for (let cur: Node | null = dom; cur; cur = parentNode(cur)) {
|
|
88
|
+
stack.push({dom: cur as HTMLElement, top: (cur as HTMLElement).scrollTop, left: (cur as HTMLElement).scrollLeft})
|
|
89
|
+
if (dom == doc) break
|
|
90
|
+
}
|
|
91
|
+
return stack
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Reset the scroll position of the editor's parent nodes to that what
|
|
95
|
+
// it was before, when storeScrollPos was called.
|
|
96
|
+
export function resetScrollPos({refDOM, refTop, stack}: {
|
|
97
|
+
refDOM: HTMLElement,
|
|
98
|
+
refTop: number,
|
|
99
|
+
stack: {dom: HTMLElement, top: number, left: number}[]
|
|
100
|
+
}) {
|
|
101
|
+
let newRefTop = refDOM ? refDOM.getBoundingClientRect().top : 0
|
|
102
|
+
restoreScrollStack(stack, newRefTop == 0 ? 0 : newRefTop - refTop)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function restoreScrollStack(stack: {dom: HTMLElement, top: number, left: number}[], dTop: number) {
|
|
106
|
+
for (let i = 0; i < stack.length; i++) {
|
|
107
|
+
let {dom, top, left} = stack[i]
|
|
108
|
+
if (dom.scrollTop != top + dTop) dom.scrollTop = top + dTop
|
|
109
|
+
if (dom.scrollLeft != left) dom.scrollLeft = left
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let preventScrollSupported: false | null | {preventScroll: boolean} = null
|
|
114
|
+
// Feature-detects support for .focus({preventScroll: true}), and uses
|
|
115
|
+
// a fallback kludge when not supported.
|
|
116
|
+
export function focusPreventScroll(dom: HTMLElement) {
|
|
117
|
+
if ((dom as any).setActive) return (dom as any).setActive() // in IE
|
|
118
|
+
if (preventScrollSupported) return dom.focus(preventScrollSupported)
|
|
119
|
+
|
|
120
|
+
let stored = scrollStack(dom)
|
|
121
|
+
dom.focus(preventScrollSupported == null ? {
|
|
122
|
+
get preventScroll() {
|
|
123
|
+
preventScrollSupported = {preventScroll: true}
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
} : undefined)
|
|
127
|
+
if (!preventScrollSupported) {
|
|
128
|
+
preventScrollSupported = false
|
|
129
|
+
restoreScrollStack(stored, 0)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function findOffsetInNode(node: HTMLElement, coords: {top: number, left: number}): {node: Node, offset: number} {
|
|
134
|
+
let closest, dxClosest = 2e8, coordsClosest: {left: number, top: number} | undefined, offset = 0
|
|
135
|
+
let rowBot = coords.top, rowTop = coords.top
|
|
136
|
+
for (let child = node.firstChild, childIndex = 0; child; child = child.nextSibling, childIndex++) {
|
|
137
|
+
let rects
|
|
138
|
+
if (child.nodeType == 1) rects = (child as HTMLElement).getClientRects()
|
|
139
|
+
else if (child.nodeType == 3) rects = textRange(child as Text).getClientRects()
|
|
140
|
+
else continue
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < rects.length; i++) {
|
|
143
|
+
let rect = rects[i]
|
|
144
|
+
if (rect.top <= rowBot && rect.bottom >= rowTop) {
|
|
145
|
+
rowBot = Math.max(rect.bottom, rowBot)
|
|
146
|
+
rowTop = Math.min(rect.top, rowTop)
|
|
147
|
+
let dx = rect.left > coords.left ? rect.left - coords.left
|
|
148
|
+
: rect.right < coords.left ? coords.left - rect.right : 0
|
|
149
|
+
if (dx < dxClosest) {
|
|
150
|
+
closest = child
|
|
151
|
+
dxClosest = dx
|
|
152
|
+
coordsClosest = dx && closest.nodeType == 3 ? {
|
|
153
|
+
left: rect.right < coords.left ? rect.right : rect.left,
|
|
154
|
+
top: coords.top
|
|
155
|
+
} : coords
|
|
156
|
+
if (child.nodeType == 1 && dx)
|
|
157
|
+
offset = childIndex + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0)
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!closest && (coords.left >= rect.right && coords.top >= rect.top ||
|
|
162
|
+
coords.left >= rect.left && coords.top >= rect.bottom))
|
|
163
|
+
offset = childIndex + 1
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (closest && closest.nodeType == 3) return findOffsetInText(closest as Text, coordsClosest!)
|
|
167
|
+
if (!closest || (dxClosest && closest.nodeType == 1)) return {node, offset}
|
|
168
|
+
return findOffsetInNode(closest as HTMLElement, coordsClosest!)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function findOffsetInText(node: Text, coords: {top: number, left: number}) {
|
|
172
|
+
let len = node.nodeValue!.length
|
|
173
|
+
let range = document.createRange()
|
|
174
|
+
for (let i = 0; i < len; i++) {
|
|
175
|
+
range.setEnd(node, i + 1)
|
|
176
|
+
range.setStart(node, i)
|
|
177
|
+
let rect = singleRect(range, 1)
|
|
178
|
+
if (rect.top == rect.bottom) continue
|
|
179
|
+
if (inRect(coords, rect))
|
|
180
|
+
return {node, offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0)}
|
|
181
|
+
}
|
|
182
|
+
return {node, offset: 0}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function inRect(coords: {top: number, left: number}, rect: Rect) {
|
|
186
|
+
return coords.left >= rect.left - 1 && coords.left <= rect.right + 1&&
|
|
187
|
+
coords.top >= rect.top - 1 && coords.top <= rect.bottom + 1
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function targetKludge(dom: HTMLElement, coords: {top: number, left: number}) {
|
|
191
|
+
let parent = dom.parentNode
|
|
192
|
+
if (parent && /^li$/i.test(parent.nodeName) && coords.left < dom.getBoundingClientRect().left)
|
|
193
|
+
return parent as HTMLElement
|
|
194
|
+
return dom
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function posFromElement(view: EditorView, elt: HTMLElement, coords: {top: number, left: number}) {
|
|
198
|
+
let {node, offset} = findOffsetInNode(elt, coords), bias = -1
|
|
199
|
+
if (node.nodeType == 1 && !node.firstChild) {
|
|
200
|
+
let rect = (node as HTMLElement).getBoundingClientRect()
|
|
201
|
+
bias = rect.left != rect.right && coords.left > (rect.left + rect.right) / 2 ? 1 : -1
|
|
202
|
+
}
|
|
203
|
+
return view.docView.posFromDOM(node, offset, bias)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function posFromCaret(view: EditorView, node: Node, offset: number, coords: {top: number, left: number}) {
|
|
207
|
+
// Browser (in caretPosition/RangeFromPoint) will agressively
|
|
208
|
+
// normalize towards nearby inline nodes. Since we are interested in
|
|
209
|
+
// positions between block nodes too, we first walk up the hierarchy
|
|
210
|
+
// of nodes to see if there are block nodes that the coordinates
|
|
211
|
+
// fall outside of. If so, we take the position before/after that
|
|
212
|
+
// block. If not, we call `posFromDOM` on the raw node/offset.
|
|
213
|
+
let outsideBlock = -1, onInlineLeaf = 1
|
|
214
|
+
for (let cur = node;;) {
|
|
215
|
+
if (cur == view.dom) break
|
|
216
|
+
let desc = view.docView.nearestDesc(cur, true)
|
|
217
|
+
if (!desc) return null
|
|
218
|
+
if (desc.node.isBlock && desc.parent) {
|
|
219
|
+
let rect = (desc.dom as HTMLElement).getBoundingClientRect()
|
|
220
|
+
if (rect.left > coords.left || rect.top > coords.top) outsideBlock = desc.posBefore
|
|
221
|
+
else if (rect.right < coords.left || rect.bottom < coords.top) outsideBlock = desc.posAfter
|
|
222
|
+
else break
|
|
223
|
+
}
|
|
224
|
+
cur = desc.dom.parentNode!
|
|
225
|
+
}
|
|
226
|
+
return outsideBlock > -1 ? outsideBlock : view.docView.posFromDOM(node, offset, 1)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function elementFromPoint(element: HTMLElement, coords: {top: number, left: number}, box: Rect): HTMLElement {
|
|
230
|
+
let len = element.childNodes.length
|
|
231
|
+
if (len && box.top < box.bottom) {
|
|
232
|
+
for (let startI = Math.max(0, Math.min(len - 1, Math.floor(len * (coords.top - box.top) / (box.bottom - box.top)) - 2)), i = startI;;) {
|
|
233
|
+
let child = element.childNodes[i]
|
|
234
|
+
if (child.nodeType == 1) {
|
|
235
|
+
let rects = (child as HTMLElement).getClientRects()
|
|
236
|
+
for (let j = 0; j < rects.length; j++) {
|
|
237
|
+
let rect = rects[j]
|
|
238
|
+
if (inRect(coords, rect)) return elementFromPoint(child as HTMLElement, coords, rect)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if ((i = (i + 1) % len) == startI) break
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return element
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Given an x,y position on the editor, get the position in the document.
|
|
248
|
+
export function posAtCoords(view: EditorView, coords: {top: number, left: number}) {
|
|
249
|
+
let doc = view.dom.ownerDocument, node: Node | undefined, offset = 0
|
|
250
|
+
if ((doc as any).caretPositionFromPoint) {
|
|
251
|
+
try { // Firefox throws for this call in hard-to-predict circumstances (#994)
|
|
252
|
+
let pos = (doc as any).caretPositionFromPoint(coords.left, coords.top)
|
|
253
|
+
if (pos) ({offsetNode: node, offset} = pos)
|
|
254
|
+
} catch (_) {}
|
|
255
|
+
}
|
|
256
|
+
if (!node && doc.caretRangeFromPoint) {
|
|
257
|
+
let range = doc.caretRangeFromPoint(coords.left, coords.top)
|
|
258
|
+
if (range) ({startContainer: node, startOffset: offset} = range)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let elt = ((view.root as any).elementFromPoint ? view.root : doc)
|
|
262
|
+
.elementFromPoint(coords.left, coords.top) as HTMLElement
|
|
263
|
+
let pos
|
|
264
|
+
if (!elt || !view.dom.contains(elt.nodeType != 1 ? elt.parentNode : elt)) {
|
|
265
|
+
let box = view.dom.getBoundingClientRect()
|
|
266
|
+
if (!inRect(coords, box)) return null
|
|
267
|
+
elt = elementFromPoint(view.dom, coords, box)
|
|
268
|
+
if (!elt) return null
|
|
269
|
+
}
|
|
270
|
+
// Safari's caretRangeFromPoint returns nonsense when on a draggable element
|
|
271
|
+
if (browser.safari) {
|
|
272
|
+
for (let p: Node | null = elt; node && p; p = parentNode(p))
|
|
273
|
+
if ((p as HTMLElement).draggable) node = undefined
|
|
274
|
+
}
|
|
275
|
+
elt = targetKludge(elt, coords)
|
|
276
|
+
if (node) {
|
|
277
|
+
if (browser.gecko && node.nodeType == 1) {
|
|
278
|
+
// Firefox will sometimes return offsets into <input> nodes, which
|
|
279
|
+
// have no actual children, from caretPositionFromPoint (#953)
|
|
280
|
+
offset = Math.min(offset, node.childNodes.length)
|
|
281
|
+
// It'll also move the returned position before image nodes,
|
|
282
|
+
// even if those are behind it.
|
|
283
|
+
if (offset < node.childNodes.length) {
|
|
284
|
+
let next = node.childNodes[offset], box
|
|
285
|
+
if (next.nodeName == "IMG" && (box = (next as HTMLElement).getBoundingClientRect()).right <= coords.left &&
|
|
286
|
+
box.bottom > coords.top)
|
|
287
|
+
offset++
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Suspiciously specific kludge to work around caret*FromPoint
|
|
291
|
+
// never returning a position at the end of the document
|
|
292
|
+
if (node == view.dom && offset == node.childNodes.length - 1 && node.lastChild!.nodeType == 1 &&
|
|
293
|
+
coords.top > (node.lastChild as HTMLElement).getBoundingClientRect().bottom)
|
|
294
|
+
pos = view.state.doc.content.size
|
|
295
|
+
// Ignore positions directly after a BR, since caret*FromPoint
|
|
296
|
+
// 'round up' positions that would be more accurately placed
|
|
297
|
+
// before the BR node.
|
|
298
|
+
else if (offset == 0 || node.nodeType != 1 || node.childNodes[offset - 1].nodeName != "BR")
|
|
299
|
+
pos = posFromCaret(view, node, offset, coords)
|
|
300
|
+
}
|
|
301
|
+
if (pos == null) pos = posFromElement(view, elt, coords)
|
|
302
|
+
|
|
303
|
+
let desc = view.docView.nearestDesc(elt, true)
|
|
304
|
+
return {pos, inside: desc ? desc.posAtStart - desc.border : -1}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function singleRect(target: HTMLElement | Range, bias: number): DOMRect {
|
|
308
|
+
let rects = target.getClientRects()
|
|
309
|
+
return !rects.length ? target.getBoundingClientRect() : rects[bias < 0 ? 0 : rects.length - 1]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const BIDI = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/
|
|
313
|
+
|
|
314
|
+
// Given a position in the document model, get a bounding box of the
|
|
315
|
+
// character at that position, relative to the window.
|
|
316
|
+
export function coordsAtPos(view: EditorView, pos: number, side: number): Rect {
|
|
317
|
+
let {node, offset, atom} = view.docView.domFromPos(pos, side < 0 ? -1 : 1)
|
|
318
|
+
|
|
319
|
+
let supportEmptyRange = browser.webkit || browser.gecko
|
|
320
|
+
if (node.nodeType == 3) {
|
|
321
|
+
// These browsers support querying empty text ranges. Prefer that in
|
|
322
|
+
// bidi context or when at the end of a node.
|
|
323
|
+
if (supportEmptyRange && (BIDI.test(node.nodeValue!) || (side < 0 ? !offset : offset == node.nodeValue!.length))) {
|
|
324
|
+
let rect = singleRect(textRange(node as Text, offset, offset), side)
|
|
325
|
+
// Firefox returns bad results (the position before the space)
|
|
326
|
+
// when querying a position directly after line-broken
|
|
327
|
+
// whitespace. Detect this situation and and kludge around it
|
|
328
|
+
if (browser.gecko && offset && /\s/.test(node.nodeValue![offset - 1]) && offset < node.nodeValue!.length) {
|
|
329
|
+
let rectBefore = singleRect(textRange(node as Text, offset - 1, offset - 1), -1)
|
|
330
|
+
if (rectBefore.top == rect.top) {
|
|
331
|
+
let rectAfter = singleRect(textRange(node as Text, offset, offset + 1), -1)
|
|
332
|
+
if (rectAfter.top != rect.top)
|
|
333
|
+
return flattenV(rectAfter, rectAfter.left < rectBefore.left)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return rect
|
|
337
|
+
} else {
|
|
338
|
+
let from = offset, to = offset, takeSide = side < 0 ? 1 : -1
|
|
339
|
+
if (side < 0 && !offset) { to++; takeSide = -1 }
|
|
340
|
+
else if (side >= 0 && offset == node.nodeValue!.length) { from--; takeSide = 1 }
|
|
341
|
+
else if (side < 0) { from-- }
|
|
342
|
+
else { to ++ }
|
|
343
|
+
return flattenV(singleRect(textRange(node as Text, from, to), 1), takeSide < 0)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let $dom = view.state.doc.resolve(pos - (atom || 0))
|
|
348
|
+
// Return a horizontal line in block context
|
|
349
|
+
if (!$dom.parent.inlineContent) {
|
|
350
|
+
if (atom == null && offset && (side < 0 || offset == nodeSize(node))) {
|
|
351
|
+
let before = node.childNodes[offset - 1]
|
|
352
|
+
if (before.nodeType == 1) return flattenH((before as HTMLElement).getBoundingClientRect(), false)
|
|
353
|
+
}
|
|
354
|
+
if (atom == null && offset < nodeSize(node)) {
|
|
355
|
+
let after = node.childNodes[offset]
|
|
356
|
+
if (after.nodeType == 1) return flattenH((after as HTMLElement).getBoundingClientRect(), true)
|
|
357
|
+
}
|
|
358
|
+
return flattenH((node as HTMLElement).getBoundingClientRect(), side >= 0)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Inline, not in text node (this is not Bidi-safe)
|
|
362
|
+
if (atom == null && offset && (side < 0 || offset == nodeSize(node))) {
|
|
363
|
+
let before = node.childNodes[offset - 1]
|
|
364
|
+
let target = before.nodeType == 3 ? textRange(before as Text, nodeSize(before) - (supportEmptyRange ? 0 : 1))
|
|
365
|
+
// BR nodes tend to only return the rectangle before them.
|
|
366
|
+
// Only use them if they are the last element in their parent
|
|
367
|
+
: before.nodeType == 1 && (before.nodeName != "BR" || !before.nextSibling) ? before : null
|
|
368
|
+
if (target) return flattenV(singleRect(target as Range | HTMLElement, 1), false)
|
|
369
|
+
}
|
|
370
|
+
if (atom == null && offset < nodeSize(node)) {
|
|
371
|
+
let after = node.childNodes[offset]
|
|
372
|
+
while (after.pmViewDesc && after.pmViewDesc.ignoreForCoords) after = after.nextSibling!
|
|
373
|
+
let target = !after ? null : after.nodeType == 3 ? textRange(after as Text, 0, (supportEmptyRange ? 0 : 1))
|
|
374
|
+
: after.nodeType == 1 ? after : null
|
|
375
|
+
if (target) return flattenV(singleRect(target as Range | HTMLElement, -1), true)
|
|
376
|
+
}
|
|
377
|
+
// All else failed, just try to get a rectangle for the target node
|
|
378
|
+
return flattenV(singleRect(node.nodeType == 3 ? textRange(node as Text) : node as HTMLElement, -side), side >= 0)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function flattenV(rect: DOMRect, left: boolean) {
|
|
382
|
+
if (rect.width == 0) return rect
|
|
383
|
+
let x = left ? rect.left : rect.right
|
|
384
|
+
return {top: rect.top, bottom: rect.bottom, left: x, right: x}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function flattenH(rect: DOMRect, top: boolean) {
|
|
388
|
+
if (rect.height == 0) return rect
|
|
389
|
+
let y = top ? rect.top : rect.bottom
|
|
390
|
+
return {top: y, bottom: y, left: rect.left, right: rect.right}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function withFlushedState<T>(view: EditorView, state: EditorState, f: () => T): T {
|
|
394
|
+
let viewState = view.state, active = view.root.activeElement as HTMLElement
|
|
395
|
+
if (viewState != state) view.updateState(state)
|
|
396
|
+
if (active != view.dom) view.focus()
|
|
397
|
+
try {
|
|
398
|
+
return f()
|
|
399
|
+
} finally {
|
|
400
|
+
if (viewState != state) view.updateState(viewState)
|
|
401
|
+
if (active != view.dom && active) active.focus()
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Whether vertical position motion in a given direction
|
|
406
|
+
// from a position would leave a text block.
|
|
407
|
+
function endOfTextblockVertical(view: EditorView, state: EditorState, dir: "up" | "down") {
|
|
408
|
+
let sel = state.selection
|
|
409
|
+
let $pos = dir == "up" ? sel.$from : sel.$to
|
|
410
|
+
return withFlushedState(view, state, () => {
|
|
411
|
+
let {node: dom} = view.docView.domFromPos($pos.pos, dir == "up" ? -1 : 1)
|
|
412
|
+
for (;;) {
|
|
413
|
+
let nearest = view.docView.nearestDesc(dom, true)
|
|
414
|
+
if (!nearest) break
|
|
415
|
+
if (nearest.node.isBlock) { dom = nearest.contentDOM || nearest.dom; break }
|
|
416
|
+
dom = nearest.dom.parentNode!
|
|
417
|
+
}
|
|
418
|
+
let coords = coordsAtPos(view, $pos.pos, 1)
|
|
419
|
+
for (let child = dom.firstChild; child; child = child.nextSibling) {
|
|
420
|
+
let boxes
|
|
421
|
+
if (child.nodeType == 1) boxes = (child as HTMLElement).getClientRects()
|
|
422
|
+
else if (child.nodeType == 3) boxes = textRange(child as Text, 0, child.nodeValue!.length).getClientRects()
|
|
423
|
+
else continue
|
|
424
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
425
|
+
let box = boxes[i]
|
|
426
|
+
if (box.bottom > box.top + 1 &&
|
|
427
|
+
(dir == "up" ? coords.top - box.top > (box.bottom - coords.top) * 2
|
|
428
|
+
: box.bottom - coords.bottom > (coords.bottom - box.top) * 2))
|
|
429
|
+
return false
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return true
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const maybeRTL = /[\u0590-\u08ac]/
|
|
437
|
+
|
|
438
|
+
function endOfTextblockHorizontal(view: EditorView, state: EditorState, dir: "left" | "right" | "forward" | "backward") {
|
|
439
|
+
let {$head} = state.selection
|
|
440
|
+
if (!$head.parent.isTextblock) return false
|
|
441
|
+
let offset = $head.parentOffset, atStart = !offset, atEnd = offset == $head.parent.content.size
|
|
442
|
+
let sel = view.domSelection()
|
|
443
|
+
// If the textblock is all LTR, or the browser doesn't support
|
|
444
|
+
// Selection.modify (Edge), fall back to a primitive approach
|
|
445
|
+
if (!maybeRTL.test($head.parent.textContent) || !(sel as any).modify)
|
|
446
|
+
return dir == "left" || dir == "backward" ? atStart : atEnd
|
|
447
|
+
|
|
448
|
+
return withFlushedState(view, state, () => {
|
|
449
|
+
// This is a huge hack, but appears to be the best we can
|
|
450
|
+
// currently do: use `Selection.modify` to move the selection by
|
|
451
|
+
// one character, and see if that moves the cursor out of the
|
|
452
|
+
// textblock (or doesn't move it at all, when at the start/end of
|
|
453
|
+
// the document).
|
|
454
|
+
let {focusNode: oldNode, focusOffset: oldOff, anchorNode, anchorOffset} = view.domSelectionRange()
|
|
455
|
+
let oldBidiLevel = (sel as any).caretBidiLevel // Only for Firefox
|
|
456
|
+
;(sel as any).modify("move", dir, "character")
|
|
457
|
+
let parentDOM = $head.depth ? view.docView.domAfterPos($head.before()) : view.dom
|
|
458
|
+
let {focusNode: newNode, focusOffset: newOff} = view.domSelectionRange()
|
|
459
|
+
let result = newNode && !parentDOM.contains(newNode.nodeType == 1 ? newNode : newNode.parentNode) ||
|
|
460
|
+
(oldNode == newNode && oldOff == newOff)
|
|
461
|
+
// Restore the previous selection
|
|
462
|
+
try {
|
|
463
|
+
sel.collapse(anchorNode, anchorOffset)
|
|
464
|
+
if (oldNode && (oldNode != anchorNode || oldOff != anchorOffset) && sel.extend) sel.extend(oldNode, oldOff)
|
|
465
|
+
} catch (_) {}
|
|
466
|
+
if (oldBidiLevel != null) (sel as any).caretBidiLevel = oldBidiLevel
|
|
467
|
+
return result
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export type TextblockDir = "up" | "down" | "left" | "right" | "forward" | "backward"
|
|
472
|
+
|
|
473
|
+
let cachedState: EditorState | null = null
|
|
474
|
+
let cachedDir: TextblockDir | null = null
|
|
475
|
+
let cachedResult: boolean = false
|
|
476
|
+
export function endOfTextblock(view: EditorView, state: EditorState, dir: TextblockDir) {
|
|
477
|
+
if (cachedState == state && cachedDir == dir) return cachedResult
|
|
478
|
+
cachedState = state; cachedDir = dir
|
|
479
|
+
return cachedResult = dir == "up" || dir == "down"
|
|
480
|
+
? endOfTextblockVertical(view, state, dir)
|
|
481
|
+
: endOfTextblockHorizontal(view, state, dir)
|
|
482
|
+
}
|
package/src/domchange.ts
CHANGED
|
@@ -81,6 +81,10 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
|
|
|
81
81
|
let origin = view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null
|
|
82
82
|
let newSel = selectionFromDOM(view, origin)
|
|
83
83
|
if (newSel && !view.state.selection.eq(newSel)) {
|
|
84
|
+
if (browser.chrome && browser.android &&
|
|
85
|
+
view.input.lastKeyCode === 13 && Date.now() - 100 < view.input.lastKeyCodeTime &&
|
|
86
|
+
view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter"))))
|
|
87
|
+
return
|
|
84
88
|
let tr = view.state.tr.setSelection(newSel)
|
|
85
89
|
if (origin == "pointer") tr.setMeta("pointer", true)
|
|
86
90
|
else if (origin == "key") tr.scrollIntoView()
|
package/src/domcoords.ts
CHANGED
|
@@ -2,7 +2,6 @@ import {EditorState} from "prosemirror-state"
|
|
|
2
2
|
import {nodeSize, textRange, parentNode} from "./dom"
|
|
3
3
|
import * as browser from "./browser"
|
|
4
4
|
import {EditorView} from "./index"
|
|
5
|
-
import {NodeViewDesc} from "./viewdesc"
|
|
6
5
|
|
|
7
6
|
export type Rect = {left: number, right: number, top: number, bottom: number}
|
|
8
7
|
|
|
@@ -216,7 +215,7 @@ function posFromCaret(view: EditorView, node: Node, offset: number, coords: {top
|
|
|
216
215
|
if (cur == view.dom) break
|
|
217
216
|
let desc = view.docView.nearestDesc(cur, true)
|
|
218
217
|
if (!desc) return null
|
|
219
|
-
if (
|
|
218
|
+
if (desc.node.isBlock && desc.parent) {
|
|
220
219
|
let rect = (desc.dom as HTMLElement).getBoundingClientRect()
|
|
221
220
|
if (rect.left > coords.left || rect.top > coords.top) outside = desc.posBefore
|
|
222
221
|
else if (rect.right < coords.left || rect.bottom < coords.top) outside = desc.posAfter
|
|
@@ -413,7 +412,7 @@ function endOfTextblockVertical(view: EditorView, state: EditorState, dir: "up"
|
|
|
413
412
|
for (;;) {
|
|
414
413
|
let nearest = view.docView.nearestDesc(dom, true)
|
|
415
414
|
if (!nearest) break
|
|
416
|
-
if (nearest.node
|
|
415
|
+
if (nearest.node.isBlock) { dom = nearest.contentDOM || nearest.dom; break }
|
|
417
416
|
dom = nearest.dom.parentNode!
|
|
418
417
|
}
|
|
419
418
|
let coords = coordsAtPos(view, $pos.pos, 1)
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {Slice, ResolvedPos, DOMParser, DOMSerializer, Node, Mark} from "prosemir
|
|
|
4
4
|
import {scrollRectIntoView, posAtCoords, coordsAtPos, endOfTextblock, storeScrollPos,
|
|
5
5
|
resetScrollPos, focusPreventScroll} from "./domcoords"
|
|
6
6
|
import {docViewDesc, ViewDesc, NodeView, NodeViewDesc} from "./viewdesc"
|
|
7
|
-
import {initInput, destroyInput, dispatchEvent, ensureListeners, clearComposition, InputState} from "./input"
|
|
7
|
+
import {initInput, destroyInput, dispatchEvent, ensureListeners, clearComposition, InputState, doPaste} from "./input"
|
|
8
8
|
import {selectionToDOM, anchorInRightPlace, syncNodeSelection} from "./selection"
|
|
9
9
|
import {Decoration, viewDecorations, DecorationSource} from "./decoration"
|
|
10
10
|
import {DOMObserver, safariShadowSelectionRange} from "./domobserver"
|
|
@@ -409,6 +409,18 @@ export class EditorView {
|
|
|
409
409
|
return endOfTextblock(this, state || this.state, dir)
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
/// Run the editor's paste logic with the given HTML string. The
|
|
413
|
+
/// `event`, if given, will be passed to the
|
|
414
|
+
/// [`handlePaste`](#view.EditorProps.handlePaste) hook.
|
|
415
|
+
pasteHTML(html: string, event?: ClipboardEvent) {
|
|
416
|
+
return doPaste(this, "", html, false, event || new ClipboardEvent("paste"))
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/// Run the editor's paste logic with the given plain-text input.
|
|
420
|
+
pasteText(text: string, event?: ClipboardEvent) {
|
|
421
|
+
return doPaste(this, text, null, true, event || new ClipboardEvent("paste"))
|
|
422
|
+
}
|
|
423
|
+
|
|
412
424
|
/// Removes the editor from the DOM and destroys all [node
|
|
413
425
|
/// views](#view.NodeView).
|
|
414
426
|
destroy() {
|
package/src/input.ts
CHANGED
|
@@ -147,7 +147,7 @@ editHandlers.keypress = (view, _event) => {
|
|
|
147
147
|
let sel = view.state.selection
|
|
148
148
|
if (!(sel instanceof TextSelection) || !sel.$from.sameParent(sel.$to)) {
|
|
149
149
|
let text = String.fromCharCode(event.charCode)
|
|
150
|
-
if (!view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text)))
|
|
150
|
+
if (!/[\r\n]/.test(text) && !view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text)))
|
|
151
151
|
view.dispatch(view.state.tr.insertText(text).scrollIntoView())
|
|
152
152
|
event.preventDefault()
|
|
153
153
|
}
|
|
@@ -583,13 +583,13 @@ function capturePaste(view: EditorView, event: ClipboardEvent) {
|
|
|
583
583
|
setTimeout(() => {
|
|
584
584
|
view.focus()
|
|
585
585
|
if (target.parentNode) target.parentNode.removeChild(target)
|
|
586
|
-
if (plainText) doPaste(view, (target as HTMLTextAreaElement).value, null, event)
|
|
587
|
-
else doPaste(view, target.textContent!, target.innerHTML, event)
|
|
586
|
+
if (plainText) doPaste(view, (target as HTMLTextAreaElement).value, null, view.input.shiftKey, event)
|
|
587
|
+
else doPaste(view, target.textContent!, target.innerHTML, view.input.shiftKey, event)
|
|
588
588
|
}, 50)
|
|
589
589
|
}
|
|
590
590
|
|
|
591
|
-
function doPaste(view: EditorView, text: string, html: string | null, event: ClipboardEvent) {
|
|
592
|
-
let slice = parseFromClipboard(view, text, html,
|
|
591
|
+
export function doPaste(view: EditorView, text: string, html: string | null, preferPlain: boolean, event: ClipboardEvent) {
|
|
592
|
+
let slice = parseFromClipboard(view, text, html, preferPlain, view.state.selection.$from)
|
|
593
593
|
if (view.someProp("handlePaste", f => f(view, event, slice || Slice.empty))) return true
|
|
594
594
|
if (!slice) return false
|
|
595
595
|
|
|
@@ -609,8 +609,10 @@ editHandlers.paste = (view, _event) => {
|
|
|
609
609
|
// where the editor is almost always composing.
|
|
610
610
|
if (view.composing && !browser.android) return
|
|
611
611
|
let data = brokenClipboardAPI ? null : event.clipboardData
|
|
612
|
-
if (data && doPaste(view, data.getData("text/plain"), data.getData("text/html"), event))
|
|
613
|
-
|
|
612
|
+
if (data && doPaste(view, data.getData("text/plain"), data.getData("text/html"), view.input.shiftKey, event))
|
|
613
|
+
event.preventDefault()
|
|
614
|
+
else
|
|
615
|
+
capturePaste(view, event)
|
|
614
616
|
}
|
|
615
617
|
|
|
616
618
|
class Dragging {
|
|
@@ -633,7 +635,7 @@ handlers.dragstart = (view, _event) => {
|
|
|
633
635
|
view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, mouseDown.mightDrag.pos)))
|
|
634
636
|
} else if (event.target && (event.target as HTMLElement).nodeType == 1) {
|
|
635
637
|
let desc = view.docView.nearestDesc(event.target as HTMLElement, true)
|
|
636
|
-
if (desc && desc.node
|
|
638
|
+
if (desc && desc.node.type.spec.draggable && desc != view.docView)
|
|
637
639
|
view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, desc.posBefore)))
|
|
638
640
|
}
|
|
639
641
|
let slice = view.state.selection.content(), {dom, text} = serializeForClipboard(view, slice)
|
package/src/viewdesc.ts
CHANGED
|
@@ -221,6 +221,8 @@ export class ViewDesc {
|
|
|
221
221
|
|
|
222
222
|
// Scan up the dom finding the first desc that is a descendant of
|
|
223
223
|
// this one.
|
|
224
|
+
nearestDesc(dom: DOMNode): ViewDesc | undefined
|
|
225
|
+
nearestDesc(dom: DOMNode, onlyNodes: true): NodeViewDesc | undefined
|
|
224
226
|
nearestDesc(dom: DOMNode, onlyNodes: boolean = false) {
|
|
225
227
|
for (let first = true, cur: DOMNode | null = dom; cur; cur = cur.parentNode) {
|
|
226
228
|
let desc = this.getDesc(cur), nodeDOM
|
|
@@ -1150,7 +1152,8 @@ class ViewTreeUpdater {
|
|
|
1150
1152
|
this.stack.push(this.top, this.index + 1)
|
|
1151
1153
|
let found = -1
|
|
1152
1154
|
for (let i = this.index; i < Math.min(this.index + 3, this.top.children.length); i++) {
|
|
1153
|
-
|
|
1155
|
+
let next = this.top.children[i]
|
|
1156
|
+
if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) { found = i; break }
|
|
1154
1157
|
}
|
|
1155
1158
|
if (found > -1) {
|
|
1156
1159
|
if (found > this.index) {
|
|
@@ -1230,7 +1233,7 @@ class ViewTreeUpdater {
|
|
|
1230
1233
|
// Can't update if nextDOM is or contains this.lock, except if
|
|
1231
1234
|
// it's a text node whose content already matches the new text
|
|
1232
1235
|
// and whose decorations match the new ones.
|
|
1233
|
-
let locked = this.
|
|
1236
|
+
let locked = this.isLocked(nextDOM) &&
|
|
1234
1237
|
!(node.isText && next.node && next.node.isText && next.nodeDOM.nodeValue == node.text &&
|
|
1235
1238
|
next.dirty != NODE_DIRTY && sameOuterDeco(outerDeco, next.outerDeco))
|
|
1236
1239
|
if (!locked && next.update(node, outerDeco, innerDeco, view)) {
|
|
@@ -1299,6 +1302,10 @@ class ViewTreeUpdater {
|
|
|
1299
1302
|
this.changed = true
|
|
1300
1303
|
}
|
|
1301
1304
|
}
|
|
1305
|
+
|
|
1306
|
+
isLocked(node: DOMNode) {
|
|
1307
|
+
return this.lock && (node == this.lock || node.nodeType == 1 && node.contains(this.lock.parentNode))
|
|
1308
|
+
}
|
|
1302
1309
|
}
|
|
1303
1310
|
|
|
1304
1311
|
// Iterate from the end of the fragment and array of descs to find
|