prosemirror-view 1.29.2 → 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 +6 -0
- package/dist/index.cjs +14 -4
- package/dist/index.d.ts +56 -1
- package/dist/index.js +19 -7
- package/package.json +1 -1
- package/src/#domcoords.ts# +482 -0
- package/src/domcoords.ts +2 -3
- package/src/index.ts +13 -1
- package/src/input.ts +9 -7
- package/src/viewdesc.ts +2 -0
package/CHANGELOG.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -3795,12 +3795,12 @@ function capturePaste(view, event) {
|
|
|
3795
3795
|
setTimeout(function () {
|
|
3796
3796
|
view.focus();
|
|
3797
3797
|
if (target.parentNode) target.parentNode.removeChild(target);
|
|
3798
|
-
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);
|
|
3799
3799
|
}, 50);
|
|
3800
3800
|
}
|
|
3801
3801
|
|
|
3802
|
-
function doPaste(view, text, html, event) {
|
|
3803
|
-
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);
|
|
3804
3804
|
if (view.someProp("handlePaste", function (f) {
|
|
3805
3805
|
return f(view, event, slice || prosemirrorModel.Slice.empty);
|
|
3806
3806
|
})) return true;
|
|
@@ -3815,7 +3815,7 @@ editHandlers.paste = function (view, _event) {
|
|
|
3815
3815
|
var event = _event;
|
|
3816
3816
|
if (view.composing && !android) return;
|
|
3817
3817
|
var data = brokenClipboardAPI ? null : event.clipboardData;
|
|
3818
|
-
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);
|
|
3819
3819
|
};
|
|
3820
3820
|
|
|
3821
3821
|
var Dragging = _createClass(function Dragging(slice, move) {
|
|
@@ -5772,6 +5772,16 @@ var EditorView = function () {
|
|
|
5772
5772
|
value: function endOfTextblock(dir, state) {
|
|
5773
5773
|
return _endOfTextblock(this, state || this.state, dir);
|
|
5774
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
|
+
}
|
|
5775
5785
|
}, {
|
|
5776
5786
|
key: "destroy",
|
|
5777
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
|
@@ -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;
|
|
@@ -3345,13 +3343,13 @@ function capturePaste(view, event) {
|
|
|
3345
3343
|
if (target.parentNode)
|
|
3346
3344
|
target.parentNode.removeChild(target);
|
|
3347
3345
|
if (plainText)
|
|
3348
|
-
doPaste(view, target.value, null, event);
|
|
3346
|
+
doPaste(view, target.value, null, view.input.shiftKey, event);
|
|
3349
3347
|
else
|
|
3350
|
-
doPaste(view, target.textContent, target.innerHTML, event);
|
|
3348
|
+
doPaste(view, target.textContent, target.innerHTML, view.input.shiftKey, event);
|
|
3351
3349
|
}, 50);
|
|
3352
3350
|
}
|
|
3353
|
-
function doPaste(view, text, html, event) {
|
|
3354
|
-
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);
|
|
3355
3353
|
if (view.someProp("handlePaste", f => f(view, event, slice || Slice.empty)))
|
|
3356
3354
|
return true;
|
|
3357
3355
|
if (!slice)
|
|
@@ -3372,7 +3370,7 @@ editHandlers.paste = (view, _event) => {
|
|
|
3372
3370
|
if (view.composing && !android)
|
|
3373
3371
|
return;
|
|
3374
3372
|
let data = brokenClipboardAPI ? null : event.clipboardData;
|
|
3375
|
-
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))
|
|
3376
3374
|
event.preventDefault();
|
|
3377
3375
|
else
|
|
3378
3376
|
capturePaste(view, event);
|
|
@@ -5262,6 +5260,20 @@ class EditorView {
|
|
|
5262
5260
|
return endOfTextblock(this, state || this.state, dir);
|
|
5263
5261
|
}
|
|
5264
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
|
+
/**
|
|
5265
5277
|
Removes the editor from the DOM and destroys all [node
|
|
5266
5278
|
views](https://prosemirror.net/docs/ref/#view.NodeView).
|
|
5267
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/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
|
@@ -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
|