prosemirror-view 1.30.0 → 1.30.2

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,21 @@
1
+ ## 1.30.2 (2023-03-13)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix an issue where `posAtCoords` could incorrectly return positions outside of a table when querying a coordinate inside it.
6
+
7
+ Update the type of the `getPos` callback to include undefined in its return type. Update broken link (ViewProps -> EditorProps) Also detect iOS enter when the new block type is a less common node
8
+
9
+ Fix an issue where Enter on iOS would sometimes not be handled by the library.
10
+
11
+ ## 1.30.1 (2023-02-04)
12
+
13
+ ### Bug fixes
14
+
15
+ Fix a bug that caused `posAtCoords` to always return the position after a leaf node when querying a position on top of the node's element.
16
+
17
+ Fix an issue where, when putting focus on the editor by clicking at the very start of the document, the editor would restore a previous selection.
18
+
1
19
  ## 1.30.0 (2023-01-19)
2
20
 
3
21
  ### New features
package/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (C) 2015-2017 by Marijn Haverbeke <marijnh@gmail.com> and others
1
+ Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
package/dist/index.cjs CHANGED
@@ -386,22 +386,31 @@ function posFromElement(view, elt, coords) {
386
386
  }
387
387
 
388
388
  function posFromCaret(view, node, offset, coords) {
389
- var outside = -1;
389
+ var outsideBlock = -1;
390
390
 
391
- for (var cur = node;;) {
391
+ for (var cur = node, sawBlock = false;;) {
392
392
  if (cur == view.dom) break;
393
393
  var desc = view.docView.nearestDesc(cur, true);
394
394
  if (!desc) return null;
395
395
 
396
- if (desc.node.isBlock && desc.parent) {
396
+ if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent && !sawBlock || !desc.contentDOM)) {
397
397
  var rect = desc.dom.getBoundingClientRect();
398
- if (rect.left > coords.left || rect.top > coords.top) outside = desc.posBefore;else if (rect.right < coords.left || rect.bottom < coords.top) outside = desc.posAfter;else break;
398
+
399
+ if (desc.node.isBlock && desc.parent && !sawBlock) {
400
+ sawBlock = true;
401
+ if (rect.left > coords.left || rect.top > coords.top) outsideBlock = desc.posBefore;else if (rect.right < coords.left || rect.bottom < coords.top) outsideBlock = desc.posAfter;
402
+ }
403
+
404
+ if (!desc.contentDOM && outsideBlock < 0) {
405
+ var before = desc.node.isBlock ? coords.top < (rect.top + rect.bottom) / 2 : coords.left < (rect.left + rect.right) / 2;
406
+ return before ? desc.posBefore : desc.posAfter;
407
+ }
399
408
  }
400
409
 
401
410
  cur = desc.dom.parentNode;
402
411
  }
403
412
 
404
- return outside > -1 ? outside : view.docView.posFromDOM(node, offset, 1);
413
+ return outsideBlock > -1 ? outsideBlock : view.docView.posFromDOM(node, offset, -1);
405
414
  }
406
415
 
407
416
  function elementFromPoint(element, coords, box) {
@@ -4943,7 +4952,7 @@ var DOMObserver = function () {
4943
4952
 
4944
4953
  var readSel = null;
4945
4954
 
4946
- if (from < 0 && newSel && view.input.lastFocus > Date.now() - 200 && view.input.lastTouch < Date.now() - 300 && selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) && readSel.eq(prosemirrorState.Selection.near(view.state.doc.resolve(0), 1))) {
4955
+ if (from < 0 && newSel && view.input.lastFocus > Date.now() - 200 && Math.max(view.input.lastTouch, view.input.lastClick.time) < Date.now() - 300 && selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) && readSel.eq(prosemirrorState.Selection.near(view.state.doc.resolve(0), 1))) {
4947
4956
  view.input.lastFocus = 0;
4948
4957
  selectionToDOM(view);
4949
4958
  this.currentSelection.set(sel);
@@ -5161,6 +5170,8 @@ function ruleFromNode(dom) {
5161
5170
  return null;
5162
5171
  }
5163
5172
 
5173
+ var isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;
5174
+
5164
5175
  function readDOMChange(view, from, to, typeOver, addedNodes) {
5165
5176
  if (from < 0) {
5166
5177
  var origin = view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null;
@@ -5202,7 +5213,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
5202
5213
  var change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide);
5203
5214
 
5204
5215
  if ((ios && view.input.lastIOSEnter > Date.now() - 225 || android) && addedNodes.some(function (n) {
5205
- return n.nodeName == "DIV" || n.nodeName == "P" || n.nodeName == "LI";
5216
+ return n.nodeType == 1 && !isInline.test(n.nodeName);
5206
5217
  }) && (!change || change.endA >= change.endB) && view.someProp("handleKeyDown", function (f) {
5207
5218
  return f(view, keyEvent(13, "Enter"));
5208
5219
  })) {
package/dist/index.d.ts CHANGED
@@ -601,12 +601,12 @@ declare class EditorView {
601
601
  dispatch(tr: Transaction): void;
602
602
  }
603
603
  /**
604
- The type of function [provided](https://prosemirror.net/docs/ref/#view.ViewProps.nodeViews) to
604
+ The type of function [provided](https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews) to
605
605
  create [node views](https://prosemirror.net/docs/ref/#view.NodeView).
606
606
  */
607
- declare type NodeViewConstructor = (node: Node, view: EditorView, getPos: () => number, decorations: readonly Decoration[], innerDecorations: DecorationSource) => NodeView;
607
+ declare type NodeViewConstructor = (node: Node, view: EditorView, getPos: () => number | undefined, decorations: readonly Decoration[], innerDecorations: DecorationSource) => NodeView;
608
608
  /**
609
- The function types [used](https://prosemirror.net/docs/ref/#view.ViewProps.markViews) to create
609
+ The function types [used](https://prosemirror.net/docs/ref/#view.EditorProps.markViews) to create
610
610
  mark views.
611
611
  */
612
612
  declare type MarkViewConstructor = (mark: Mark, view: EditorView, inline: boolean) => {
@@ -767,6 +767,8 @@ interface EditorProps<P = any> {
767
767
  node's display behavior. The third argument `getPos` is a
768
768
  function that can be called to get the node's current position,
769
769
  which can be useful when creating transactions to update it.
770
+ Note that if the node is not in the document, the position
771
+ returned by this function will be `undefined`.
770
772
 
771
773
  `decorations` is an array of node or inline decorations that are
772
774
  active around the node. They are automatically drawn in the
@@ -782,7 +784,7 @@ interface EditorProps<P = any> {
782
784
  inner decorations.
783
785
 
784
786
  (For backwards compatibility reasons, [mark
785
- views](https://prosemirror.net/docs/ref/#view.ViewProps.markViews) can also be included in this
787
+ views](https://prosemirror.net/docs/ref/#view.EditorProps.markViews) can also be included in this
786
788
  object.)
787
789
  */
788
790
  nodeViews?: {
package/dist/index.js CHANGED
@@ -314,25 +314,32 @@ function posFromCaret(view, node, offset, coords) {
314
314
  // of nodes to see if there are block nodes that the coordinates
315
315
  // fall outside of. If so, we take the position before/after that
316
316
  // block. If not, we call `posFromDOM` on the raw node/offset.
317
- let outside = -1;
318
- for (let cur = node;;) {
317
+ let outsideBlock = -1;
318
+ for (let cur = node, sawBlock = false;;) {
319
319
  if (cur == view.dom)
320
320
  break;
321
321
  let desc = view.docView.nearestDesc(cur, true);
322
322
  if (!desc)
323
323
  return null;
324
- if (desc.node.isBlock && desc.parent) {
324
+ if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent && !sawBlock || !desc.contentDOM)) {
325
325
  let rect = desc.dom.getBoundingClientRect();
326
- if (rect.left > coords.left || rect.top > coords.top)
327
- outside = desc.posBefore;
328
- else if (rect.right < coords.left || rect.bottom < coords.top)
329
- outside = desc.posAfter;
330
- else
331
- break;
326
+ if (desc.node.isBlock && desc.parent && !sawBlock) {
327
+ sawBlock = true;
328
+ if (rect.left > coords.left || rect.top > coords.top)
329
+ outsideBlock = desc.posBefore;
330
+ else if (rect.right < coords.left || rect.bottom < coords.top)
331
+ outsideBlock = desc.posAfter;
332
+ }
333
+ if (!desc.contentDOM && outsideBlock < 0) {
334
+ // If we are inside a leaf, return the side of the leaf closer to the coords
335
+ let before = desc.node.isBlock ? coords.top < (rect.top + rect.bottom) / 2
336
+ : coords.left < (rect.left + rect.right) / 2;
337
+ return before ? desc.posBefore : desc.posAfter;
338
+ }
332
339
  }
333
340
  cur = desc.dom.parentNode;
334
341
  }
335
- return outside > -1 ? outside : view.docView.posFromDOM(node, offset, 1);
342
+ return outsideBlock > -1 ? outsideBlock : view.docView.posFromDOM(node, offset, -1);
336
343
  }
337
344
  function elementFromPoint(element, coords, box) {
338
345
  let len = element.childNodes.length;
@@ -4379,7 +4386,7 @@ class DOMObserver {
4379
4386
  // start of the document after focus, restore the selection from
4380
4387
  // the state
4381
4388
  if (from < 0 && newSel && view.input.lastFocus > Date.now() - 200 &&
4382
- view.input.lastTouch < Date.now() - 300 &&
4389
+ Math.max(view.input.lastTouch, view.input.lastClick.time) < Date.now() - 300 &&
4383
4390
  selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) &&
4384
4391
  readSel.eq(Selection.near(view.state.doc.resolve(0), 1))) {
4385
4392
  view.input.lastFocus = 0;
@@ -4568,6 +4575,7 @@ function ruleFromNode(dom) {
4568
4575
  }
4569
4576
  return null;
4570
4577
  }
4578
+ const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;
4571
4579
  function readDOMChange(view, from, to, typeOver, addedNodes) {
4572
4580
  if (from < 0) {
4573
4581
  let origin = view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null;
@@ -4606,7 +4614,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4606
4614
  view.input.lastKeyCode = null;
4607
4615
  let change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide);
4608
4616
  if ((ios && view.input.lastIOSEnter > Date.now() - 225 || android) &&
4609
- addedNodes.some(n => n.nodeName == "DIV" || n.nodeName == "P" || n.nodeName == "LI") &&
4617
+ addedNodes.some(n => n.nodeType == 1 && !isInline.test(n.nodeName)) &&
4610
4618
  (!change || change.endA >= change.endB) &&
4611
4619
  view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) {
4612
4620
  view.input.lastIOSEnter = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.30.0",
3
+ "version": "1.30.2",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -19,7 +19,7 @@
19
19
  "maintainers": [
20
20
  {
21
21
  "name": "Marijn Haverbeke",
22
- "email": "marijnh@gmail.com",
22
+ "email": "marijn@haverbeke.berlin",
23
23
  "web": "http://marijnhaverbeke.nl"
24
24
  }
25
25
  ],
package/src/domchange.ts CHANGED
@@ -76,6 +76,8 @@ function ruleFromNode(dom: DOMNode): ParseRule | null {
76
76
  return null
77
77
  }
78
78
 
79
+ const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i
80
+
79
81
  export function readDOMChange(view: EditorView, from: number, to: number, typeOver: boolean, addedNodes: readonly DOMNode[]) {
80
82
  if (from < 0) {
81
83
  let origin = view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null
@@ -115,7 +117,7 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
115
117
 
116
118
  let change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide)
117
119
  if ((browser.ios && view.input.lastIOSEnter > Date.now() - 225 || browser.android) &&
118
- addedNodes.some(n => n.nodeName == "DIV" || n.nodeName == "P" || n.nodeName == "LI") &&
120
+ addedNodes.some(n => n.nodeType == 1 && !isInline.test(n.nodeName)) &&
119
121
  (!change || change.endA >= change.endB) &&
120
122
  view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) {
121
123
  view.input.lastIOSEnter = 0
package/src/domcoords.ts CHANGED
@@ -210,20 +210,28 @@ function posFromCaret(view: EditorView, node: Node, offset: number, coords: {top
210
210
  // of nodes to see if there are block nodes that the coordinates
211
211
  // fall outside of. If so, we take the position before/after that
212
212
  // block. If not, we call `posFromDOM` on the raw node/offset.
213
- let outside = -1
214
- for (let cur = node;;) {
213
+ let outsideBlock = -1
214
+ for (let cur = node, sawBlock = false;;) {
215
215
  if (cur == view.dom) break
216
216
  let desc = view.docView.nearestDesc(cur, true)
217
217
  if (!desc) return null
218
- if (desc.node.isBlock && desc.parent) {
218
+ if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent && !sawBlock || !desc.contentDOM)) {
219
219
  let rect = (desc.dom as HTMLElement).getBoundingClientRect()
220
- if (rect.left > coords.left || rect.top > coords.top) outside = desc.posBefore
221
- else if (rect.right < coords.left || rect.bottom < coords.top) outside = desc.posAfter
222
- else break
220
+ if (desc.node.isBlock && desc.parent && !sawBlock) {
221
+ sawBlock = true
222
+ if (rect.left > coords.left || rect.top > coords.top) outsideBlock = desc.posBefore
223
+ else if (rect.right < coords.left || rect.bottom < coords.top) outsideBlock = desc.posAfter
224
+ }
225
+ if (!desc.contentDOM && outsideBlock < 0) {
226
+ // If we are inside a leaf, return the side of the leaf closer to the coords
227
+ let before = desc.node.isBlock ? coords.top < (rect.top + rect.bottom) / 2
228
+ : coords.left < (rect.left + rect.right) / 2
229
+ return before ? desc.posBefore : desc.posAfter
230
+ }
223
231
  }
224
232
  cur = desc.dom.parentNode!
225
233
  }
226
- return outside > -1 ? outside : view.docView.posFromDOM(node, offset, 1)
234
+ return outsideBlock > -1 ? outsideBlock : view.docView.posFromDOM(node, offset, -1)
227
235
  }
228
236
 
229
237
  function elementFromPoint(element: HTMLElement, coords: {top: number, left: number}, box: Rect): HTMLElement {
@@ -195,7 +195,7 @@ export class DOMObserver {
195
195
  // start of the document after focus, restore the selection from
196
196
  // the state
197
197
  if (from < 0 && newSel && view.input.lastFocus > Date.now() - 200 &&
198
- view.input.lastTouch < Date.now() - 300 &&
198
+ Math.max(view.input.lastTouch, view.input.lastClick.time) < Date.now() - 300 &&
199
199
  selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) &&
200
200
  readSel.eq(Selection.near(view.state.doc.resolve(0), 1))) {
201
201
  view.input.lastFocus = 0
package/src/index.ts CHANGED
@@ -544,12 +544,12 @@ function checkStateComponent(plugin: Plugin) {
544
544
  throw new RangeError("Plugins passed directly to the view must not have a state component")
545
545
  }
546
546
 
547
- /// The type of function [provided](#view.ViewProps.nodeViews) to
547
+ /// The type of function [provided](#view.EditorProps.nodeViews) to
548
548
  /// create [node views](#view.NodeView).
549
- export type NodeViewConstructor = (node: Node, view: EditorView, getPos: () => number,
549
+ export type NodeViewConstructor = (node: Node, view: EditorView, getPos: () => number | undefined,
550
550
  decorations: readonly Decoration[], innerDecorations: DecorationSource) => NodeView
551
551
 
552
- /// The function types [used](#view.ViewProps.markViews) to create
552
+ /// The function types [used](#view.EditorProps.markViews) to create
553
553
  /// mark views.
554
554
  export type MarkViewConstructor = (mark: Mark, view: EditorView, inline: boolean) => {dom: HTMLElement, contentDOM?: HTMLElement}
555
555
 
@@ -684,6 +684,8 @@ export interface EditorProps<P = any> {
684
684
  /// node's display behavior. The third argument `getPos` is a
685
685
  /// function that can be called to get the node's current position,
686
686
  /// which can be useful when creating transactions to update it.
687
+ /// Note that if the node is not in the document, the position
688
+ /// returned by this function will be `undefined`.
687
689
  ///
688
690
  /// `decorations` is an array of node or inline decorations that are
689
691
  /// active around the node. They are automatically drawn in the
@@ -699,7 +701,7 @@ export interface EditorProps<P = any> {
699
701
  /// inner decorations.
700
702
  ///
701
703
  /// (For backwards compatibility reasons, [mark
702
- /// views](#view.ViewProps.markViews) can also be included in this
704
+ /// views](#view.EditorProps.markViews) can also be included in this
703
705
  /// object.)
704
706
  nodeViews?: {[node: string]: NodeViewConstructor}
705
707
 
@@ -1,482 +0,0 @@
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
- }