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/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
- updateSelection(view, TextSelection.create(doc, nodePos + 1, nodePos + 1 + node.content.size), "pointer")
264
- else if (NodeSelection.isSelectable(node))
265
- updateSelection(view, NodeSelection.create(doc, nodePos), "pointer")
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
- if (view.input.mouseDown) view.input.mouseDown.done()
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
- class MouseDown {
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
- readonly view: EditorView,
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
- this.view.root.removeEventListener("mouseup", this.up as any)
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(() => selectionToDOM(this.view))
371
- this.view.input.mouseDown = null
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
- if (event.buttons == 0) this.done()
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(event.timeStamp - view.input.compositionEndedAt) < 500) {
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 = event.timeStamp
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 = timestampFromCustomEvent()
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
- // The delayed drag selection causes issues with Cell Selections
62
- // in Safari. And the drag selection delay is to workarond issues
63
- // which only present in Chrome.
64
- if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && browser.chrome) {
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
- view.input.mouseDown.delayedSelectionSync = true
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, readonly mark: Mark, dom: HTMLElement, contentDOM: HTMLElement, readonly spec: MarkView) {
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, view, pos + 1)
723
+ nodeDOM as HTMLElement, spec)
719
724
  else if (node.isText)
720
- return new TextViewDesc(parent, node, outerDeco, innerDeco, dom, nodeDOM, view)
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, view, pos + 1)
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, view, 0)
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, view: EditorView) {
917
- super(parent, node, outerDeco, innerDeco, dom, null, nodeDOM, view, 0)
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, view: EditorView) {
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, view)
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
- view: EditorView, pos: number) {
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