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 +18 -0
- package/LICENSE +1 -1
- package/dist/index.cjs +18 -7
- package/dist/index.d.ts +6 -4
- package/dist/index.js +20 -12
- package/package.json +2 -2
- package/src/domchange.ts +3 -1
- package/src/domcoords.ts +15 -7
- package/src/domobserver.ts +1 -1
- package/src/index.ts +6 -4
- package/src/#domcoords.ts# +0 -482
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 <
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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 (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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": "
|
|
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.
|
|
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
|
|
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 (
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
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 {
|
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/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.
|
|
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.
|
|
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.
|
|
704
|
+
/// views](#view.EditorProps.markViews) can also be included in this
|
|
703
705
|
/// object.)
|
|
704
706
|
nodeViews?: {[node: string]: NodeViewConstructor}
|
|
705
707
|
|
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
|
-
}
|