prosemirror-view 1.30.0 → 1.30.1
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 +8 -0
- package/LICENSE +1 -1
- package/dist/index.cjs +13 -5
- package/dist/index.js +16 -10
- package/package.json +2 -2
- package/src/domcoords.ts +13 -6
- package/src/domobserver.ts +1 -1
- package/src/#domcoords.ts# +0 -482
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 1.30.1 (2023-02-04)
|
|
2
|
+
|
|
3
|
+
### Bug fixes
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
1
9
|
## 1.30.0 (2023-01-19)
|
|
2
10
|
|
|
3
11
|
### New features
|
package/LICENSE
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright (C) 2015-2017 by Marijn Haverbeke <
|
|
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,30 @@ function posFromElement(view, elt, coords) {
|
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
function posFromCaret(view, node, offset, coords) {
|
|
389
|
-
var
|
|
389
|
+
var outsideBlock = -1;
|
|
390
390
|
|
|
391
391
|
for (var cur = node;;) {
|
|
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 || !desc.contentDOM)) {
|
|
397
397
|
var rect = desc.dom.getBoundingClientRect();
|
|
398
|
-
|
|
398
|
+
|
|
399
|
+
if (desc.node.isBlock && desc.parent) {
|
|
400
|
+
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;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!desc.contentDOM && outsideBlock < 0) {
|
|
404
|
+
var before = desc.node.isBlock ? coords.top < (rect.top + rect.bottom) / 2 : coords.left < (rect.left + rect.right) / 2;
|
|
405
|
+
return before ? desc.posBefore : desc.posAfter;
|
|
406
|
+
}
|
|
399
407
|
}
|
|
400
408
|
|
|
401
409
|
cur = desc.dom.parentNode;
|
|
402
410
|
}
|
|
403
411
|
|
|
404
|
-
return
|
|
412
|
+
return outsideBlock > -1 ? outsideBlock : view.docView.posFromDOM(node, offset, -1);
|
|
405
413
|
}
|
|
406
414
|
|
|
407
415
|
function elementFromPoint(element, coords, box) {
|
|
@@ -4943,7 +4951,7 @@ var DOMObserver = function () {
|
|
|
4943
4951
|
|
|
4944
4952
|
var readSel = null;
|
|
4945
4953
|
|
|
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))) {
|
|
4954
|
+
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
4955
|
view.input.lastFocus = 0;
|
|
4948
4956
|
selectionToDOM(view);
|
|
4949
4957
|
this.currentSelection.set(sel);
|
package/dist/index.js
CHANGED
|
@@ -314,25 +314,31 @@ 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
|
|
317
|
+
let outsideBlock = -1;
|
|
318
318
|
for (let cur = node;;) {
|
|
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 || !desc.contentDOM)) {
|
|
325
325
|
let rect = desc.dom.getBoundingClientRect();
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
326
|
+
if (desc.node.isBlock && desc.parent) {
|
|
327
|
+
if (rect.left > coords.left || rect.top > coords.top)
|
|
328
|
+
outsideBlock = desc.posBefore;
|
|
329
|
+
else if (rect.right < coords.left || rect.bottom < coords.top)
|
|
330
|
+
outsideBlock = desc.posAfter;
|
|
331
|
+
}
|
|
332
|
+
if (!desc.contentDOM && outsideBlock < 0) {
|
|
333
|
+
// If we are inside a leaf, return the side of the leaf closer to the coords
|
|
334
|
+
let before = desc.node.isBlock ? coords.top < (rect.top + rect.bottom) / 2
|
|
335
|
+
: coords.left < (rect.left + rect.right) / 2;
|
|
336
|
+
return before ? desc.posBefore : desc.posAfter;
|
|
337
|
+
}
|
|
332
338
|
}
|
|
333
339
|
cur = desc.dom.parentNode;
|
|
334
340
|
}
|
|
335
|
-
return
|
|
341
|
+
return outsideBlock > -1 ? outsideBlock : view.docView.posFromDOM(node, offset, -1);
|
|
336
342
|
}
|
|
337
343
|
function elementFromPoint(element, coords, box) {
|
|
338
344
|
let len = element.childNodes.length;
|
|
@@ -4379,7 +4385,7 @@ class DOMObserver {
|
|
|
4379
4385
|
// start of the document after focus, restore the selection from
|
|
4380
4386
|
// the state
|
|
4381
4387
|
if (from < 0 && newSel && view.input.lastFocus > Date.now() - 200 &&
|
|
4382
|
-
view.input.lastTouch < Date.now() - 300 &&
|
|
4388
|
+
Math.max(view.input.lastTouch, view.input.lastClick.time) < Date.now() - 300 &&
|
|
4383
4389
|
selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) &&
|
|
4384
4390
|
readSel.eq(Selection.near(view.state.doc.resolve(0), 1))) {
|
|
4385
4391
|
view.input.lastFocus = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prosemirror-view",
|
|
3
|
-
"version": "1.30.
|
|
3
|
+
"version": "1.30.1",
|
|
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": "
|
|
22
|
+
"email": "marijn@haverbeke.berlin",
|
|
23
23
|
"web": "http://marijnhaverbeke.nl"
|
|
24
24
|
}
|
|
25
25
|
],
|
package/src/domcoords.ts
CHANGED
|
@@ -210,20 +210,27 @@ 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
|
|
213
|
+
let outsideBlock = -1
|
|
214
214
|
for (let cur = node;;) {
|
|
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 || !desc.contentDOM)) {
|
|
219
219
|
let rect = (desc.dom as HTMLElement).getBoundingClientRect()
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
if (desc.node.isBlock && desc.parent) {
|
|
221
|
+
if (rect.left > coords.left || rect.top > coords.top) outsideBlock = desc.posBefore
|
|
222
|
+
else if (rect.right < coords.left || rect.bottom < coords.top) outsideBlock = desc.posAfter
|
|
223
|
+
}
|
|
224
|
+
if (!desc.contentDOM && outsideBlock < 0) {
|
|
225
|
+
// If we are inside a leaf, return the side of the leaf closer to the coords
|
|
226
|
+
let before = desc.node.isBlock ? coords.top < (rect.top + rect.bottom) / 2
|
|
227
|
+
: coords.left < (rect.left + rect.right) / 2
|
|
228
|
+
return before ? desc.posBefore : desc.posAfter
|
|
229
|
+
}
|
|
223
230
|
}
|
|
224
231
|
cur = desc.dom.parentNode!
|
|
225
232
|
}
|
|
226
|
-
return
|
|
233
|
+
return outsideBlock > -1 ? outsideBlock : view.docView.posFromDOM(node, offset, -1)
|
|
227
234
|
}
|
|
228
235
|
|
|
229
236
|
function elementFromPoint(element: HTMLElement, coords: {top: number, left: number}, box: Rect): HTMLElement {
|
package/src/domobserver.ts
CHANGED
|
@@ -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/#domcoords.ts#
DELETED
|
@@ -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
|
-
}
|