prosemirror-view 1.41.8 → 1.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/index.cjs +231 -146
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +136 -72
- package/package.json +2 -2
- package/src/clipboard.ts +14 -3
- package/src/domchange.ts +3 -13
- package/src/domobserver.ts +4 -1
- package/src/index.ts +3 -2
- package/src/input.ts +88 -34
- package/src/selection.ts +6 -6
- package/src/viewdesc.ts +31 -14
package/src/input.ts
CHANGED
|
@@ -66,6 +66,7 @@ function setSelectionOrigin(view: EditorView, origin: string) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export function destroyInput(view: EditorView) {
|
|
69
|
+
if (view.input.mouseDown) view.input.mouseDown.done()
|
|
69
70
|
view.domObserver.stop()
|
|
70
71
|
for (let type in view.input.eventHandlers)
|
|
71
72
|
view.dom.removeEventListener(type, view.input.eventHandlers[type])
|
|
@@ -246,27 +247,28 @@ function handleTripleClick(view: EditorView, pos: number, inside: number, event:
|
|
|
246
247
|
|
|
247
248
|
function defaultTripleClick(view: EditorView, inside: number, event: MouseEvent) {
|
|
248
249
|
if (event.button != 0) return false
|
|
250
|
+
let selection = selectionForTripleClick(view, inside, true), doc = view.state.doc
|
|
251
|
+
if (!selection) return false
|
|
252
|
+
updateSelection(view, selection, "pointer")
|
|
253
|
+
if (selection instanceof TextSelection && doc.eq(view.state.doc))
|
|
254
|
+
view.input.mouseDown = new TripleClickDrag(view, selection)
|
|
255
|
+
return true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function selectionForTripleClick(view: EditorView, inside: number, selectNodes: boolean): Selection | null {
|
|
249
259
|
let doc = view.state.doc
|
|
250
|
-
if (inside == -1)
|
|
251
|
-
if (doc.inlineContent) {
|
|
252
|
-
updateSelection(view, TextSelection.create(doc, 0, doc.content.size), "pointer")
|
|
253
|
-
return true
|
|
254
|
-
}
|
|
255
|
-
return false
|
|
256
|
-
}
|
|
260
|
+
if (inside == -1) return doc.inlineContent ? TextSelection.create(doc, 0, doc.content.size) : null
|
|
257
261
|
|
|
258
262
|
let $pos = doc.resolve(inside)
|
|
259
263
|
for (let i = $pos.depth + 1; i > 0; i--) {
|
|
260
264
|
let node = i > $pos.depth ? $pos.nodeAfter! : $pos.node(i)
|
|
261
265
|
let nodePos = $pos.before(i)
|
|
262
266
|
if (node.inlineContent)
|
|
263
|
-
|
|
264
|
-
else if (NodeSelection.isSelectable(node))
|
|
265
|
-
|
|
266
|
-
else
|
|
267
|
-
continue
|
|
268
|
-
return true
|
|
267
|
+
return TextSelection.create(doc, nodePos + 1, nodePos + 1 + node.content.size)
|
|
268
|
+
else if (selectNodes && NodeSelection.isSelectable(node))
|
|
269
|
+
return NodeSelection.create(doc, nodePos)
|
|
269
270
|
}
|
|
271
|
+
return null
|
|
270
272
|
}
|
|
271
273
|
|
|
272
274
|
function forceDOMFlush(view: EditorView) {
|
|
@@ -287,12 +289,13 @@ handlers.mousedown = (view, _event) => {
|
|
|
287
289
|
}
|
|
288
290
|
view.input.lastClick = {time: now, x: event.clientX, y: event.clientY, type, button: event.button}
|
|
289
291
|
|
|
292
|
+
if (view.input.mouseDown) view.input.mouseDown.done()
|
|
293
|
+
|
|
290
294
|
let pos = view.posAtCoords(eventCoords(event))
|
|
291
295
|
if (!pos) return
|
|
292
296
|
|
|
293
297
|
if (type == "singleClick") {
|
|
294
|
-
|
|
295
|
-
view.input.mouseDown = new MouseDown(view, pos, event, !!flushed)
|
|
298
|
+
view.input.mouseDown = new LeftMouseDown(view, pos, event, !!flushed)
|
|
296
299
|
} else if ((type == "doubleClick" ? handleDoubleClick : handleTripleClick)(view, pos.pos, pos.inside, event)) {
|
|
297
300
|
event.preventDefault()
|
|
298
301
|
} else {
|
|
@@ -300,20 +303,46 @@ handlers.mousedown = (view, _event) => {
|
|
|
300
303
|
}
|
|
301
304
|
}
|
|
302
305
|
|
|
303
|
-
|
|
306
|
+
|
|
307
|
+
abstract class MouseDown {
|
|
308
|
+
mightDrag: {node: Node, pos: number, addAttr: boolean, setUneditable: boolean} | null = null
|
|
309
|
+
|
|
310
|
+
constructor(readonly view: EditorView) {
|
|
311
|
+
view.root.addEventListener("mouseup", this.up = this.up.bind(this) as any)
|
|
312
|
+
view.root.addEventListener("mousemove", this.move = this.move.bind(this) as any)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
up(event: MouseEvent) {
|
|
316
|
+
this.done()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
move(event: MouseEvent) {
|
|
320
|
+
if (event.buttons == 0) this.done()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
done() {
|
|
324
|
+
this.view.root.removeEventListener("mouseup", this.up as any)
|
|
325
|
+
this.view.root.removeEventListener("mousemove", this.move as any)
|
|
326
|
+
if (this.view.input.mouseDown == this) this.view.input.mouseDown = null
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
delaySelUpdate() { return false }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
class LeftMouseDown extends MouseDown {
|
|
304
333
|
startDoc: Node
|
|
305
334
|
selectNode: boolean
|
|
306
335
|
allowDefault: boolean
|
|
307
336
|
delayedSelectionSync = false
|
|
308
|
-
mightDrag: {node: Node, pos: number, addAttr: boolean, setUneditable: boolean} | null = null
|
|
309
337
|
target: HTMLElement | null
|
|
310
338
|
|
|
311
339
|
constructor(
|
|
312
|
-
|
|
340
|
+
view: EditorView,
|
|
313
341
|
readonly pos: {pos: number, inside: number},
|
|
314
342
|
readonly event: MouseEvent,
|
|
315
343
|
readonly flushed: boolean
|
|
316
344
|
) {
|
|
345
|
+
super(view)
|
|
317
346
|
this.startDoc = view.state.doc
|
|
318
347
|
this.selectNode = !!event[selectNodeModifier]
|
|
319
348
|
this.allowDefault = event.shiftKey
|
|
@@ -353,22 +382,20 @@ class MouseDown {
|
|
|
353
382
|
this.view.domObserver.start()
|
|
354
383
|
}
|
|
355
384
|
|
|
356
|
-
view.root.addEventListener("mouseup", this.up = this.up.bind(this) as any)
|
|
357
|
-
view.root.addEventListener("mousemove", this.move = this.move.bind(this) as any)
|
|
358
385
|
setSelectionOrigin(view, "pointer")
|
|
359
386
|
}
|
|
360
387
|
|
|
361
388
|
done() {
|
|
362
|
-
|
|
363
|
-
this.view.root.removeEventListener("mousemove", this.move as any)
|
|
389
|
+
super.done()
|
|
364
390
|
if (this.mightDrag && this.target) {
|
|
365
391
|
this.view.domObserver.stop()
|
|
366
392
|
if (this.mightDrag.addAttr) this.target.removeAttribute("draggable")
|
|
367
393
|
if (this.mightDrag.setUneditable) this.target.removeAttribute("contentEditable")
|
|
368
394
|
this.view.domObserver.start()
|
|
369
395
|
}
|
|
370
|
-
if (this.delayedSelectionSync) setTimeout(() =>
|
|
371
|
-
|
|
396
|
+
if (this.delayedSelectionSync) setTimeout(() => {
|
|
397
|
+
if (!this.view.isDestroyed) selectionToDOM(this.view)
|
|
398
|
+
})
|
|
372
399
|
}
|
|
373
400
|
|
|
374
401
|
up(event: MouseEvent) {
|
|
@@ -409,7 +436,7 @@ class MouseDown {
|
|
|
409
436
|
move(event: MouseEvent) {
|
|
410
437
|
this.updateAllowDefault(event)
|
|
411
438
|
setSelectionOrigin(this.view, "pointer")
|
|
412
|
-
|
|
439
|
+
super.move(event)
|
|
413
440
|
}
|
|
414
441
|
|
|
415
442
|
updateAllowDefault(event: MouseEvent) {
|
|
@@ -417,6 +444,39 @@ class MouseDown {
|
|
|
417
444
|
Math.abs(this.event.y - event.clientY) > 4))
|
|
418
445
|
this.allowDefault = true
|
|
419
446
|
}
|
|
447
|
+
|
|
448
|
+
delaySelUpdate() {
|
|
449
|
+
if (!this.allowDefault) return false
|
|
450
|
+
this.delayedSelectionSync = true
|
|
451
|
+
return true
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
class TripleClickDrag extends MouseDown {
|
|
456
|
+
startDoc: Node
|
|
457
|
+
|
|
458
|
+
constructor(view: EditorView, readonly startSelection: TextSelection) {
|
|
459
|
+
super(view)
|
|
460
|
+
this.startDoc = view.state.doc
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
move(event: MouseEvent) {
|
|
464
|
+
if (event.buttons == 0 || this.view.isDestroyed || !this.view.state.doc.eq(this.startDoc)) {
|
|
465
|
+
this.done()
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
event.preventDefault()
|
|
470
|
+
setSelectionOrigin(this.view, "pointer")
|
|
471
|
+
|
|
472
|
+
let pos = this.view.posAtCoords(eventCoords(event))
|
|
473
|
+
let target = pos && selectionForTripleClick(this.view, pos.inside, false)
|
|
474
|
+
if (!target) return
|
|
475
|
+
|
|
476
|
+
let {doc} = this.view.state, start = this.startSelection
|
|
477
|
+
let [anchor, head] = target.from < start.from ? [start.to, target.from] : [start.from, target.to]
|
|
478
|
+
updateSelection(this.view, TextSelection.create(doc, anchor, head), "pointer")
|
|
479
|
+
}
|
|
420
480
|
}
|
|
421
481
|
|
|
422
482
|
handlers.touchstart = view => {
|
|
@@ -444,7 +504,7 @@ function inOrNearComposition(view: EditorView, event: Event) {
|
|
|
444
504
|
// This guards against the case where compositionend is triggered without the keyboard
|
|
445
505
|
// (e.g. character confirmation may be done with the mouse), and keydown is triggered
|
|
446
506
|
// afterwards- we wouldn't want to ignore the keydown event in this case.
|
|
447
|
-
if (browser.safari && Math.abs(
|
|
507
|
+
if (browser.safari && Math.abs(Date.now() - view.input.compositionEndedAt) < 500) {
|
|
448
508
|
view.input.compositionEndedAt = -2e8
|
|
449
509
|
return true
|
|
450
510
|
}
|
|
@@ -502,7 +562,7 @@ function selectionBeforeUneditable(view: EditorView) {
|
|
|
502
562
|
editHandlers.compositionend = (view, event) => {
|
|
503
563
|
if (view.composing) {
|
|
504
564
|
view.input.composing = false
|
|
505
|
-
view.input.compositionEndedAt =
|
|
565
|
+
view.input.compositionEndedAt = Date.now()
|
|
506
566
|
view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0
|
|
507
567
|
view.input.compositionNode = null
|
|
508
568
|
if (view.input.badSafariComposition) view.domObserver.forceFlush()
|
|
@@ -520,7 +580,7 @@ function scheduleComposeEnd(view: EditorView, delay: number) {
|
|
|
520
580
|
export function clearComposition(view: EditorView) {
|
|
521
581
|
if (view.composing) {
|
|
522
582
|
view.input.composing = false
|
|
523
|
-
view.input.compositionEndedAt =
|
|
583
|
+
view.input.compositionEndedAt = Date.now()
|
|
524
584
|
}
|
|
525
585
|
while (view.input.compositionNodes.length > 0) view.input.compositionNodes.pop()!.markParentsDirty()
|
|
526
586
|
}
|
|
@@ -544,12 +604,6 @@ export function findCompositionNode(view: EditorView) {
|
|
|
544
604
|
return textBefore || textAfter
|
|
545
605
|
}
|
|
546
606
|
|
|
547
|
-
function timestampFromCustomEvent() {
|
|
548
|
-
let event = document.createEvent("Event")
|
|
549
|
-
event.initEvent("event", true, true)
|
|
550
|
-
return event.timeStamp
|
|
551
|
-
}
|
|
552
|
-
|
|
553
607
|
/// @internal
|
|
554
608
|
export function endComposition(view: EditorView, restarting = false) {
|
|
555
609
|
if (browser.android && view.domObserver.flushingSoon >= 0) return
|
package/src/selection.ts
CHANGED
|
@@ -58,15 +58,15 @@ export function selectionToDOM(view: EditorView, force = false) {
|
|
|
58
58
|
|
|
59
59
|
if (!editorOwnsSelection(view)) return
|
|
60
60
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
if (!force &&
|
|
61
|
+
// Need to delay selection normalization during a native selection
|
|
62
|
+
// drag on Chrome, or it will cause further dragging to glitch.
|
|
63
|
+
let mouseDown = view.input.mouseDown
|
|
64
|
+
if (!force && browser.chrome && mouseDown) {
|
|
65
65
|
let domSel = view.domSelectionRange(), curSel = view.domObserver.currentSelection
|
|
66
66
|
if (domSel.anchorNode && curSel.anchorNode &&
|
|
67
67
|
isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset,
|
|
68
|
-
curSel.anchorNode, curSel.anchorOffset)
|
|
69
|
-
|
|
68
|
+
curSel.anchorNode, curSel.anchorOffset) &&
|
|
69
|
+
mouseDown.delaySelUpdate()) {
|
|
70
70
|
view.domObserver.setCurSelection()
|
|
71
71
|
return
|
|
72
72
|
}
|
package/src/viewdesc.ts
CHANGED
|
@@ -105,6 +105,13 @@ export interface MarkView {
|
|
|
105
105
|
/// present, the `dom` property is used as the content DOM.
|
|
106
106
|
contentDOM?: HTMLElement | null
|
|
107
107
|
|
|
108
|
+
/// When given, this is called when the view is updating itself. It
|
|
109
|
+
/// will be given a mark (of the same type as the current one). When it
|
|
110
|
+
/// returns true, the existing DOM is kept and reused (its content is
|
|
111
|
+
/// still managed by ProseMirror); when it returns false, the mark view
|
|
112
|
+
/// is rebuilt.
|
|
113
|
+
update?: (mark: Mark) => boolean
|
|
114
|
+
|
|
108
115
|
/// Called when a [mutation](#view.ViewMutationRecord) happens within the
|
|
109
116
|
/// view. Return false if the editor should re-read the selection or re-parse
|
|
110
117
|
/// the range around the mutation, true if it can safely be ignored.
|
|
@@ -610,7 +617,7 @@ class CompositionViewDesc extends ViewDesc {
|
|
|
610
617
|
// some cases they will be split more often than would appear
|
|
611
618
|
// necessary.
|
|
612
619
|
class MarkViewDesc extends ViewDesc {
|
|
613
|
-
constructor(parent: ViewDesc,
|
|
620
|
+
constructor(parent: ViewDesc, public mark: Mark, dom: HTMLElement, contentDOM: HTMLElement, readonly spec: MarkView) {
|
|
614
621
|
super(parent, [], dom, contentDOM)
|
|
615
622
|
}
|
|
616
623
|
|
|
@@ -672,8 +679,6 @@ export class NodeViewDesc extends ViewDesc {
|
|
|
672
679
|
dom: DOMNode,
|
|
673
680
|
contentDOM: HTMLElement | null,
|
|
674
681
|
readonly nodeDOM: DOMNode,
|
|
675
|
-
view: EditorView,
|
|
676
|
-
pos: number
|
|
677
682
|
) {
|
|
678
683
|
super(parent, [], dom, contentDOM)
|
|
679
684
|
}
|
|
@@ -715,11 +720,11 @@ export class NodeViewDesc extends ViewDesc {
|
|
|
715
720
|
|
|
716
721
|
if (spec)
|
|
717
722
|
return descObj = new CustomNodeViewDesc(parent, node, outerDeco, innerDeco, dom as HTMLElement, contentDOM || null,
|
|
718
|
-
nodeDOM as HTMLElement, spec
|
|
723
|
+
nodeDOM as HTMLElement, spec)
|
|
719
724
|
else if (node.isText)
|
|
720
|
-
return new TextViewDesc(parent, node, outerDeco, innerDeco, dom, nodeDOM
|
|
725
|
+
return new TextViewDesc(parent, node, outerDeco, innerDeco, dom, nodeDOM)
|
|
721
726
|
else
|
|
722
|
-
return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM
|
|
727
|
+
return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM)
|
|
723
728
|
}
|
|
724
729
|
|
|
725
730
|
parseRule() {
|
|
@@ -906,15 +911,15 @@ export class NodeViewDesc extends ViewDesc {
|
|
|
906
911
|
export function docViewDesc(doc: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource,
|
|
907
912
|
dom: HTMLElement, view: EditorView): NodeViewDesc {
|
|
908
913
|
applyOuterDeco(dom, outerDeco, doc)
|
|
909
|
-
let docView = new NodeViewDesc(undefined, doc, outerDeco, innerDeco, dom, dom, dom
|
|
914
|
+
let docView = new NodeViewDesc(undefined, doc, outerDeco, innerDeco, dom, dom, dom)
|
|
910
915
|
if (docView.contentDOM) docView.updateChildren(view, 0)
|
|
911
916
|
return docView
|
|
912
917
|
}
|
|
913
918
|
|
|
914
919
|
class TextViewDesc extends NodeViewDesc {
|
|
915
920
|
constructor(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[],
|
|
916
|
-
innerDeco: DecorationSource, dom: DOMNode, nodeDOM: DOMNode
|
|
917
|
-
super(parent, node, outerDeco, innerDeco, dom, null, nodeDOM
|
|
921
|
+
innerDeco: DecorationSource, dom: DOMNode, nodeDOM: DOMNode) {
|
|
922
|
+
super(parent, node, outerDeco, innerDeco, dom, null, nodeDOM)
|
|
918
923
|
}
|
|
919
924
|
|
|
920
925
|
parseRule() {
|
|
@@ -955,9 +960,9 @@ class TextViewDesc extends NodeViewDesc {
|
|
|
955
960
|
return mutation.type != "characterData" && mutation.type != "selection"
|
|
956
961
|
}
|
|
957
962
|
|
|
958
|
-
slice(from: number, to: number,
|
|
963
|
+
slice(from: number, to: number, _view?: EditorView) {
|
|
959
964
|
let node = this.node.cut(from, to), dom = document.createTextNode(node.text!)
|
|
960
|
-
return new TextViewDesc(this.parent, node, this.outerDeco, this.innerDeco, dom, dom
|
|
965
|
+
return new TextViewDesc(this.parent, node, this.outerDeco, this.innerDeco, dom, dom)
|
|
961
966
|
}
|
|
962
967
|
|
|
963
968
|
markDirty(from: number, to: number) {
|
|
@@ -985,9 +990,8 @@ class TrailingHackViewDesc extends ViewDesc {
|
|
|
985
990
|
// customized.
|
|
986
991
|
class CustomNodeViewDesc extends NodeViewDesc {
|
|
987
992
|
constructor(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource,
|
|
988
|
-
dom: HTMLElement, contentDOM: HTMLElement | null, nodeDOM: HTMLElement, readonly spec: NodeView
|
|
989
|
-
|
|
990
|
-
super(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, view, pos)
|
|
993
|
+
dom: HTMLElement, contentDOM: HTMLElement | null, nodeDOM: HTMLElement, readonly spec: NodeView) {
|
|
994
|
+
super(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM)
|
|
991
995
|
}
|
|
992
996
|
|
|
993
997
|
// A custom `update` method gets to decide whether the update goes
|
|
@@ -1220,6 +1224,19 @@ class ViewTreeUpdater {
|
|
|
1220
1224
|
let next = this.top.children[i]
|
|
1221
1225
|
if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) { found = i; break }
|
|
1222
1226
|
}
|
|
1227
|
+
// When nothing matches, try to update the mark view at this position
|
|
1228
|
+
// in place, so a custom mark view can adapt to a changed mark without
|
|
1229
|
+
// re-creating its DOM.
|
|
1230
|
+
if (found < 0 && this.index < this.top.children.length) {
|
|
1231
|
+
let cur = this.top.children[this.index]
|
|
1232
|
+
if (cur instanceof MarkViewDesc && cur.dirty != NODE_DIRTY &&
|
|
1233
|
+
cur.mark.type == marks[depth].type && cur.spec.update &&
|
|
1234
|
+
!this.isLocked(cur.dom) && cur.spec.update(marks[depth])) {
|
|
1235
|
+
cur.mark = marks[depth]
|
|
1236
|
+
found = this.index
|
|
1237
|
+
this.changed = true
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1223
1240
|
if (found > -1) {
|
|
1224
1241
|
if (found > this.index) {
|
|
1225
1242
|
this.changed = true
|