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 CHANGED
@@ -1,3 +1,9 @@
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
+
1
7
  ## 1.29.2 (2023-01-17)
2
8
 
3
9
  ### Bug fixes
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, view.input.shiftKey, view.state.selection.$from);
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, onlyNodes?: boolean): ViewDesc | undefined;
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, view.input.shiftKey, view.state.selection.$from);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.29.2",
3
+ "version": "1.30.0",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -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 ((desc as NodeViewDesc).node.isBlock && desc.parent) {
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!.isBlock) { dom = nearest.contentDOM || nearest.dom; break }
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, view.input.shiftKey, view.state.selection.$from)
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)) event.preventDefault()
613
- else capturePaste(view, event)
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!.type.spec.draggable && desc != view.docView)
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